* * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage accounts */ namespace EGroupware\Api\Accounts; use EGroupware\Api; /** * Univention LDAP Backend for accounts * * This backend is mostly identical to LDAP backend and need to be configured in the same way. * * Only difference is that new users get created via univention-directory-manager CLI program, * to generate necesary Kerberos stuff. * * New groups are generated via same CLI, if we have an ID/RID to set. * * Existing users and groups need to be renamed via same CLI, as removing and * adding entry under new dn via LDAP fails (Type or value exists). */ class Univention extends Ldap { /** * Attribute with mail address */ const MAIL_ATTR = 'mailprimaryaddress'; /** * Name of binary to call * * It is a symlink to /usr/share/univention-directory-manager-tools/directory-manager-cli. * Both directories must be included in open_basedir! */ const DIRECTORY_MANAGER_BIN = '/usr/sbin/univention-directory-manager'; /** * Saves / adds the data of one account * * If no account_id is set in data the account is added and the new id is set in $data. * * @param array $data array with account-data * @return int|boolean the account_id or false on error */ function save(&$data) { // UCS lowercases email when storing $data['account_email'] = strtolower($data['account_email']); if (self::available()) { $ssh = null;//'/usr/bin/ssh -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" -i /var/lib/egroupware/id_rsa root@10.44.22.194'; $config = $this->frontend->config && $this->frontend->config['ldap_context'] ? $this->frontend->config : $GLOBALS['egw_info']['server']; if ($data['account_type'] !== 'g' && (empty($data['account_id']) || !$this->id2name($data['account_id']))) { // empty names give an error: The property Last/First name is required is not valid if (empty($data['account_firstname'])) $data['account_firstname'] = 'n/a'; if (empty($data['account_lastname'])) $data['account_lastname'] = 'n/a'; $params = array( 'users/user','create', '--binddn', $config['ldap_root_dn'], '--bindpwd', 5=>$config['ldap_root_pw'], '--position', $config['ldap_context'], '--set', 'username='.$data['account_lid'], '--set', 'firstname='.$data['account_firstname'], '--set', 'lastname='.$data['account_lastname'], ); // we can't create a new user without a password, setting a randowm one for now $matches = null; if (empty($data['account_passwd']) || preg_match('/^{([a-z0-9_]+)}/i', $data['account_passwd'], $matches)) { if ($matches && strtolower($matches[1]) === 'plain') { $data['account_passwd'] = substr($data['account_passwd'], 7); } else { $data['account_passwd'] = Api\Auth::randomstring(12); //file_put_contents('/tmp/passwords', "$data[account_lid]\t$data[account_passwd]\n", FILE_APPEND); } } $params[] = '--set'; $params[] = 'password='.$data['account_passwd']; // if account_id is given and bigger then 1000, set it to facilitate migration if (!empty($data['account_id']) && $data['account_id'] >= Ads::MIN_ACCOUNT_ID) { $params[] = '--set'; $params[] = 'uidNumber='.(int)$data['account_id']; $params[] = '--set'; $params[] = 'sambaRID='.(int)$data['account_id']; } if (!empty($data['account_email'])) { $params[] = '--set'; $params[] = 'mailPrimaryAddress='.$data['account_email']; // we need to set mailHomeServer, so mailbox gets created for Dovecot // get_default() does not work for Adminstrator, try acc_id=1 instead // if everything fails try hostname ... try { if (!($account = Api\Mail\Account::get_default(false, false, false))) { $account = Api\Mail\Account::read(1); } $hostname = $account->acc_imap_host; } catch(\Exception $e) { unset($e); } //$hostname='master.test-org.intranet'; if (empty($hostname)) $hostname = trim(exec('hostname -f')); $params[] = '--set'; $params[] = 'mailHomeServer='.$hostname; } $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); if (isset($ssh)) $cmd = $ssh.' bash -c "\\"'.$cmd.'\\""'; $output_arr = $ret = $matches = null; exec($cmd, $output_arr, $ret); $output = implode("\n", $output_arr); if ($ret || !preg_match('/^Object created: (uid=.*)$/mui', $output, $matches)) { $params[5] = '********'; // mask out password! $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); throw new Api\Exception\WrongUserinput($cmd."\nreturned\n".$output); } $data['account_dn'] = $matches[1]; $data['account_id'] = $this->name2id($data['account_lid'], 'account_lid', 'u'); } // create new groups with given account_id via directory-manager too, to be able to set the RID elseif($data['account_type'] === 'g' && !empty($data['account_id']) && $data['account_id'] >= Ads::MIN_ACCOUNT_ID && !$this->id2name($data['account_id'])) { $params = array( 'groups/group', 'create', '--binddn', $config['ldap_root_dn'], '--bindpwd', 5=>$config['ldap_root_pw'], '--position', empty($config['ldap_group_context']) ? $config['ldap_context'] : $config['ldap_group_context'], '--set', 'name='.$data['account_lid'], '--set', 'gidNumber='.(int)$data['account_id'], '--set', 'sambaRID='.(int)$data['account_id'], ); $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); if (isset($ssh)) $cmd = $ssh.' bash -c "\\"'.$cmd.'\\""'; $output_arr = $ret = $matches = null; exec($cmd, $output_arr, $ret); $output = implode("\n", $output_arr); if ($ret || !preg_match('/^Object created: (cn=.*)$/mui', $output, $matches)) { $params[5] = '********'; // mask out password! $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); throw new Api\Exception\WrongUserinput($cmd."\nreturned\n".$output); } $data['account_dn'] = $matches[1]; $data['account_id'] = $this->name2id($data['account_lid'], 'account_lid', 'g'); } elseif($data['account_id'] && ($data['old_loginid'] || ($data['old_loginid'] = $this->id2name($data['account_id']))) && $data['account_lid'] != $data['old_loginid'] && ($data['account_dn'] = $this->id2name($data['account_id'], 'account_dn'))) { $params = array( $data['account_type'] !== 'g' ? 'users/user' : 'groups/group', 'modify', '--binddn', $config['ldap_root_dn'], '--bindpwd', 5=>$config['ldap_root_pw'], '--dn', $data['account_dn'], '--set', ($data['account_type'] !== 'g' ? 'username' : 'name').'='.$data['account_lid'], ); $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); if (isset($ssh)) $cmd = $ssh.' bash -c "\\"'.$cmd.'\\""'; $output_arr = $ret = $matches = null; exec($cmd, $output_arr, $ret); $output = implode("\n", $output_arr); if ($ret || !preg_match('/^Object modified: ((uid|cn)=.*)$/mui', $output, $matches)) { $params[5] = '********'; // mask out password! $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); throw new Api\Exception\WrongUserinput($cmd."\nreturned\n".$output); } $data['account_dn'] = $data['account_type'] !== 'g' ? $matches[1] : // duno why but directory-manager returns old dn for groups ... preg_replace('/^cn=[^,]+,/', 'cn='.$data['account_lid'].',', $data['account_dn']); } } //else error_log(__METHOD__."() ".self::DIRECTORY_MANAGER_BIN." is NOT available!"); return parent::save($data); } /** * convert an alphanumeric account-value (account_lid, account_email) to the account_id * * Reimplement to check for users outside regular user-dn eg. functional users * * @param string $_name value to convert * @param string $which ='account_lid' type of $name: account_lid (default), account_email, person_id, account_fullname * @param string $account_type u = user, g = group, default null = try both * @return int|false numeric account_id or false on error ($name not found) */ function name2id($_name,$which='account_lid',$account_type=null) { if ((!$id = parent::name2id($_name, $which, $account_type))) { $user_dn = $this->user_context; $this->user_context = preg_replace('/(cn|uid)=([^,]+),/i', '', $this->user_context); $id = parent::name2id($_name, $which, $account_type); $this->user_context = $user_dn; } return $id; } /** * Convert an numeric account_id to any other value of that account (account_lid, account_email, ...) * * Reimplement to check for users outside regular user-dn eg. functional users * * @param int $account_id numerica account_id * @param string $which ='account_lid' type to convert to: account_lid (default), account_email, ... * @return string|false converted value or false on error ($account_id not found) */ function id2name($account_id,$which='account_lid') { if (($name = parent::id2name($account_id, $which)) === false) { if (!is_numeric($account_id)) $account_id = $this->name2id($account_id); $user_dn = $this->user_context; $this->user_context = preg_replace('/(cn|uid)=([^,]+),/i', '', $this->user_context); if ($account_id && ($data = $this->read($account_id))) { $name = $data[$which]; } $this->user_context = $user_dn; } return $name; } /** * Check if our function depending on an external binary is available * * @return boolean */ public static function available() { //return true; return file_exists(self::DIRECTORY_MANAGER_BIN) && is_executable(self::DIRECTORY_MANAGER_BIN); } /** * changes password in LDAP * * If $old_passwd is given, the password change is done binded as user and NOT with the * "root" dn given in the configurations. * * @param string $old_passwd must be cleartext or empty to not to be checked * @param string $new_passwd must be cleartext * @param int $account_id account id of user whose passwd should be changed * @param boolean $update_lastchange =true * @return boolean true if password successful changed, false otherwise */ function change_password($old_passwd, $new_passwd, $account_id=0, $update_lastchange=true) { if (!self::available()) { return false; } if (!$account_id) { $username = $GLOBALS['egw_info']['user']['account_lid']; } else { $username = Api\Translation::convert($GLOBALS['egw']->accounts->id2name($account_id), Api\Translation::charset(),'utf-8'); } if ($this->debug) error_log(__METHOD__."('$old_passwd','$new_passwd',$account_id, $update_lastchange) username='$username'"); $filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']), $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)'); $ds = $ds_admin = Api\Ldap::factory(); $sri = ldap_search($ds, $GLOBALS['egw_info']['server']['ldap_context'], $filter); $allValues = ldap_get_entries($ds, $sri); if ($update_lastchange) { // ToDo: $entry['shadowlastchange'] = round((time()-date('Z')) / (24*3600)); } $dn = $allValues[0]['dn']; if($old_passwd) // if old password given (not called by admin) --> bind as that user to change the pw { try { $ds = Api\Ldap::factory(true, '', $dn, $old_passwd); } catch (Api\Exception\NoPermission $e) { unset($e); return false; // wrong old user password } } $ssh = null;//'/usr/bin/ssh -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" -i /var/lib/egroupware/id_rsa root@10.44.22.194'; $config = $this->frontend->config && $this->frontend->config['ldap_context'] ? $this->frontend->config : $GLOBALS['egw_info']['server']; $params = array( 'users/user','modify', '--binddn', $config['ldap_root_dn'], '--bindpwd', 5=>$config['ldap_root_pw'], '--dn', $dn, '--set', 'password='.$new_passwd, ); if ($old_passwd) { $params[] = '--set'; $params[] = 'pwdChangeNextLogin=0'; } $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); if (isset($ssh)) $cmd = $ssh.' bash -c "\\"'.$cmd.'\\""'; $output_arr = $ret = $matches = null; exec($cmd, $output_arr, $ret); $output = implode("\n", $output_arr); if ($ret || !preg_match('/^Object modified: ((uid|cn)=.*)$/mui', $output, $matches)) { $params[5] = '********'; // mask out password! $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); throw new Api\Exception\WrongUserinput($cmd."\nreturned\n".$output); } if($old_passwd) // if old password given (not called by admin) update the password in the session { // using time() is sufficient to represent the current time, we do not need the timestamp written to the storage Api\Cache::setSession('phpgwapi','auth_alpwchange_val',time()); } return $new_passwd; } }