From 18a048e5c8839844119691bcffedf60294fbda92 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 3 Jul 2018 22:23:53 +0200 Subject: [PATCH] finish migration AD to new UCS domain via EGroupware --- api/src/Accounts.php | 20 +++- api/src/Accounts/Ldap.php | 32 +++++-- api/src/Accounts/Univention.php | 126 +++++++++++++++++++++++-- setup/inc/class.setup_cmd_ldap.inc.php | 25 ++--- 4 files changed, 171 insertions(+), 32 deletions(-) diff --git a/api/src/Accounts.php b/api/src/Accounts.php index da48289d56..39ffd224a9 100644 --- a/api/src/Accounts.php +++ b/api/src/Accounts.php @@ -12,8 +12,6 @@ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage accounts - * @access public - * @version $Id$ */ namespace EGroupware\Api; @@ -817,6 +815,16 @@ class Accounts { if (!$account_id || !($data = $this->read($account_id))) { + // non sql backends might NOT show EGw all users, but backend->id2name/name2id does + if (is_a($this->backend, __CLASS__.'\\Univention')) + { + if (!is_numeric($account_id) ? + ($account_id = $this->backend->name2id($account_id)) : + $this->backend->id2name($account_id)) + { + return $account_id > 0 ? 1 : 2; + } + } return 0; } return $data['account_type'] == 'u' ? 1 : 2; @@ -1152,15 +1160,19 @@ class Accounts // instance-wide cache if ($account_ids) { + $instance = self::getInstance(); + foreach((array)$account_ids as $account_id) { - $instance = self::getInstance(); - Cache::unsetCache($instance->config['install_id'], __CLASS__, 'account-'.$account_id); unset(self::$request_cache[$account_id]); } } + else + { + self::$request_cache = array(); + } // session-cache if (self::$cache) self::$cache = array(); diff --git a/api/src/Accounts/Ldap.php b/api/src/Accounts/Ldap.php index 3d18867707..0997af1584 100644 --- a/api/src/Accounts/Ldap.php +++ b/api/src/Accounts/Ldap.php @@ -14,7 +14,6 @@ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage accounts - * @version $Id$ */ namespace EGroupware\Api\Accounts; @@ -989,7 +988,7 @@ class Ldap * * @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) + * @return string|false converted value or false on error ($account_id not found) */ function id2name($account_id,$which='account_lid') { @@ -1058,7 +1057,7 @@ class Ldap foreach($group[0]['memberuid'] as $lid) { - if (($id = $this->name2id($lid, 'account_lid', 'u'))) + if (($id = $this->name2id($lid, 'account_lid'))) // also return groups! { $members[$id] = $lid; } @@ -1104,7 +1103,6 @@ class Ldap * @param int $gid gidnumber of group to set * @param array $objectclass =null should we set the member and uniqueMember attributes (groupOf(Unique)Names|univentionGroup) (default detect it) * @param string $use_cn =null if set $cn is used instead $gid and the attributes are returned, not written to ldap - * @param boolean $uniqueMember =null should we set the uniqueMember attribute (default detect it) * @return boolean/array false on failure, array or true otherwise */ function set_members($members, $gid, array $objectclass=null, $use_cn=null) @@ -1112,15 +1110,31 @@ class Ldap if (!($cn = $use_cn) && !($cn = $this->id2name($gid))) return false; // do that group is a groupOf(Unique)Names or univentionGroup? - if (is_null($objectclass)) $objectclass = $this->id2name($gid,'objectclass'); + if (!isset($objectclass)) + { + $objectclass = $this->id2name($gid, 'objectclass'); + // if we cant find objectclass, we might ge in the middle of a migration + if (!isset($objectclass)) + { + Api\Accounts::cache_invalidate($gid); + if (!($objectclass = $this->id2name($gid, 'objectclass'))) + { + // group does not yet exist --> return false + return false; + } + } + } $to_write = array('memberuid' => array()); - foreach((array)$members as $key => $member) + foreach((array)$members as $member) { + if (!$member) continue; + $member_dn = $this->id2name($member, 'account_dn'); if (is_numeric($member)) $member = $this->id2name($member); - if ($member) + // only add a member, if we have the neccessary info / he already exists in migration + if ($member && ($member_dn || !array_intersect(array('groupofnames','groupofuniquenames','univentiongroup'), $objectclass))) { $to_write['memberuid'][] = $member; if (in_array('groupofnames', $objectclass)) @@ -1154,7 +1168,7 @@ class Ldap if ($forward) { $to_write[$forward] = array(); - foreach($members as $key => $member) + foreach($members as $member) { if (($email = $this->id2name($member,'account_email'))) $to_write[$forward][] = $email; } @@ -1162,7 +1176,7 @@ class Ldap } if (!ldap_modify($this->ds,'cn='.Api\Ldap::quote($cn).','.$this->group_context,$to_write)) { - echo "ldap_modify(,'cn=$cn,$this->group_context',".print_r($to_write,true)."))\n"; + error_log(__METHOD__."(members=".array2string($members).", gid=$gid, objectclass=".array2string($objectclass).", use_cn=$use_cn) !ldap_modify(,'cn=$cn,$this->group_context', ".array2string($to_write).") --> ldap_error()=".ldap_error($this->ds)); return false; } return true; diff --git a/api/src/Accounts/Univention.php b/api/src/Accounts/Univention.php index 6750074123..72e9e5363c 100644 --- a/api/src/Accounts/Univention.php +++ b/api/src/Accounts/Univention.php @@ -8,7 +8,6 @@ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage accounts - * @version $Id$ */ namespace EGroupware\Api\Accounts; @@ -23,6 +22,8 @@ use EGroupware\Api; * 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). */ @@ -56,11 +57,16 @@ class Univention extends Ldap 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_id'] && $data['account_type'] !== 'g') + 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'], @@ -70,13 +76,34 @@ class Univention extends Ldap '--set', 'firstname='.$data['account_firstname'], '--set', 'lastname='.$data['account_lastname'], ); - if ($data['account_email']) + + // 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']; - } - if (!empty($data['account_passwd'])) - { - $params[] = '--set'; $params[] = 'password='.$data['account_passwd']; + // 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 ... @@ -90,10 +117,12 @@ class Univention extends Ldap catch(\Exception $e) { unset($e); } - if (empty($hostname)) $hostname = trim(system('hostname -f')); + //$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); @@ -106,6 +135,34 @@ class Univention extends Ldap $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'))) @@ -118,6 +175,7 @@ class Univention extends Ldap '--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); @@ -136,6 +194,57 @@ class Univention extends Ldap 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 * @@ -143,6 +252,7 @@ class Univention extends Ldap */ public static function available() { + //return true; return file_exists(self::DIRECTORY_MANAGER_BIN) && is_executable(self::DIRECTORY_MANAGER_BIN); } } diff --git a/setup/inc/class.setup_cmd_ldap.inc.php b/setup/inc/class.setup_cmd_ldap.inc.php index ee344ede35..adf8a1cce2 100644 --- a/setup/inc/class.setup_cmd_ldap.inc.php +++ b/setup/inc/class.setup_cmd_ldap.inc.php @@ -55,7 +55,7 @@ use EGroupware\Api; * 1. migrate from AD --> SQL including mail-attributes * 2. optionaly fix user-names etc in SQL * 3. migrate from SQL --> Univention (make sure NOT to select existing users like "join-backup/slaves" - * and delete "anonymous" user from EGroupware App install in UCS) + * and delete "anonymous" user and "Admins", "Default" and "NoGroup" from EGroupware App install in UCS) */ class setup_cmd_ldap extends setup_cmd { @@ -683,24 +683,27 @@ class setup_cmd_ldap extends setup_cmd ($errors || $this->verbose ? "\n- ".implode("\n- ",$msg) : ''); } // migrate addressbook data - $GLOBALS['egw_info']['user']['apps']['admin'] = true; // otherwise migration will not run in setup! - $addressbook = new Api\Contacts\Storage(); - foreach($this->as_array() as $name => $value) + if ($to !== 'univention') // ToDo: univentions currently gives only Oject class violation { - if (substr($name, 5) == 'ldap_' || substr($name, 4) == 'ads_') + $GLOBALS['egw_info']['user']['apps']['admin'] = true; // otherwise migration will not run in setup! + $addressbook = new Api\Contacts\Storage(); + foreach($this->as_array() as $name => $value) { - $GLOBALS['egw_info']['server'][$name] = $value; + if (substr($name, 5) == 'ldap_' || substr($name, 4) == 'ads_') + { + $GLOBALS['egw_info']['server'][$name] = $value; + } } + ob_start(); + $addressbook->migrate2ldap($to != 'sql' ? 'accounts' : 'accounts-back'. + ($this->account_repository == 'ads' ? '-ads' : '')); + $msg = array_merge($msg, explode("\n", strip_tags(ob_get_clean()))); } - ob_start(); - $addressbook->migrate2ldap($to != 'sql' ? 'accounts' : 'accounts-back'. - ($this->account_repository == 'ads' ? '-ads' : '')); - $msgs = array_merge($msg, explode("\n", strip_tags(ob_get_clean()))); $this->restore_db(); return lang('%1 users and %2 groups created, %3 errors',$accounts_created,$groups_created,$errors). - ($errors || $this->verbose ? "\n- ".implode("\n- ",$msgs) : ''); + ($errors || $this->verbose ? "\n- ".implode("\n- ",$msg) : ''); } /**