complete rewrite in 6/2006 * * This class replaces the former accounts_ldap class written by * Joseph Engo , Lars Kneschke , * Miles Lott and Bettina Gille . * Copyright (C) 2000 - 2002 Joseph Engo, Lars Kneschke * Copyright (C) 2003 Lars Kneschke, Bettina Gille * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage accounts * @version $Id$ */ /** * LDAP Backend for accounts * * @author Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage accounts * @access internal only use the interface provided by the accounts class */ class accounts_backend { /** * resource with connection to the ldap server * * @var resource */ var $ds; /** * LDAP context for users, eg. ou=account,dc=domain,dc=com * * @var string */ var $user_context; /** * LDAP context for groups, eg. ou=groups,dc=domain,dc=com * * @var string */ var $group_context; /** * total number of found entries from get_list method * * @var int */ var $total; /** * required classe for user and groups * * @var array */ var $requiredObjectClasses = array( 'user' => array( 'top','person','organizationalperson','inetorgperson','posixaccount','shadowaccount' ), 'group' => array( 'top','posixgroup', // some newer ldap require namedObject here, as none of the above is a structural object there // this gets now autodetected //'namedObject' ) ); /** * reference to the translation class * * @var object */ var $translation; /** * Constructor * * @return accounts_backend */ function accounts_backend() { // enable the caching in the session, done by the accounts class extending this class. $this->use_session_cache = true; $this->ds = $GLOBALS['egw']->common->ldapConnect(); if(!@is_object($GLOBALS['egw']->translation)) { $GLOBALS['egw']->translation =& CreateObject('phpgwapi.translation'); } $this->translation =& $GLOBALS['egw']->translation; $this->user_context = $GLOBALS['egw_info']['server']['ldap_context']; $this->group_context = $GLOBALS['egw_info']['server']['ldap_group_context'] ? $GLOBALS['egw_info']['server']['ldap_group_context'] : $GLOBALS['egw_info']['server']['ldap_context']; } /** * Reads the data of one account * * @param int $account_id numeric account-id * @return array/boolean array with account data (keys: account_id, account_lid, ...) or false if account not found */ function read($account_id) { if (!(int)$account_id) return false; if ($account_id < 0) { return $this->_read_group($account_id); } return $this->_read_user($account_id); } /** * 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) { $is_group = $data['account_id'] < 0 || $data['account_type'] === 'g'; $data_utf8 = $this->translation->convert($data,$this->translation->charset(),'utf-8'); // common code for users and groups // checks if accout_lid (dn) has been changed or required objectclass'es are missing if ($data_utf8['account_id'] && $data_utf8['account_lid']) { // read the entry first, to check if the dn (account_lid) has changed $sri = $is_group ? ldap_search($this->ds,$this->group_context,'gidnumber='.abs($data['account_id'])) : ldap_search($this->ds,$this->user_context,'uidnumber='.$data['account_id']); $old = ldap_get_entries($this->ds, $sri); if (!$old['count']) { unset($old); } else { $old = $this->_ldap2array($old[0]); foreach($old['objectclass'] as $n => $class) { $old['objectclass'][$n] = strtolower($class); } if ($is_group && ($old['cn'] != $data_utf8['account_lid'] || substr($old['dn'],0,3) != 'cn=') || !$is_group && ($old['uid'] != $data_utf8['account_lid'] || substr($old['dn'],0,4) != 'uid=')) { // query the memberships to set them again later if (!$is_group) $memberships = $this->memberships($data['account_id']); // if dn has changed --> delete the old entry, as we cant rename the dn // $this->delete would call accounts::delete, which will delete als ACL of the user too! accounts_backend::delete($data['account_id']); unset($old['dn']); $to_write = $old; unset($old); } } } if (!$data['account_id']) // new group { if (!($data['account_id'] = $data_utf8['account_id'] = $this->_get_nextid($is_group ? 'g' : 'u'))) { return false; } } // check if we need to write the objectclass: new entry or required object classes are missing if (!$old || array_diff($this->requiredObjectClasses[$is_group ? 'group' : 'user'],$old['objectclass'])) { // additional objectclasse might be already set in $to_write or $old if (!is_array($to_write['objectclass'])) { $to_write['objectclass'] = $old ? $old['objectclass'] : array(); } $to_write['objectclass'] = array_values(array_unique(array_merge($to_write['objectclass'], $this->requiredObjectClasses[$is_group ? 'group' : 'user']))); } if (!($dn = $old['dn'])) { if (!$data['account_lid']) return false; $dn = $is_group ? 'cn='.$data_utf8['account_lid'].','.$this->group_context : 'uid='.$data_utf8['account_lid'].','.$this->user_context; } // now we merge the user or group data if ($is_group) { $to_write = $this->_merge_group($to_write,$data_utf8); $data['account_type'] = 'g'; } else { $to_write = $this->_merge_user($to_write,$data_utf8,!$old); $data['account_type'] = 'u'; } //echo "

ldap_".($old ? 'modify' : 'add')."(,$dn,".print_r($to_write,true).")

\n"; // modifying or adding the entry if ($old && !ldap_modify($this->ds,$dn,$to_write) || !$old && !ldap_add($this->ds,$dn,$to_write)) { $err = true; if (!$old && $is_group) { $to_write['objectclass'][] = 'namedobject'; $err = !ldap_add($this->ds,$dn,$to_write); } if ($err) { echo "ldap_".($old ? 'modify' : 'add')."(,$dn,".print_r($to_write,true).")\n"; echo ldap_error($this->ds); return false; } } if ($memberships) // setting the previous memberships of the renamed account { $this->set_memberships($memberships,$data['account_id']); } return $data['account_id']; } /** * Convert a single ldap value into a associative array * * @param array $ldap array with numerical and associative indexes and count's * @return array with only associative index and no count's */ function _ldap2array($ldap) { if (!is_array($ldap)) return false; $arr = array(); foreach($ldap as $var => $val) { if (is_int($var) || $var == 'count') continue; if (is_array($val) && $val['count'] == 1) { $arr[$var] = $val[0]; } else { if (is_array($val)) unset($val['count']); $arr[$var] = $val; } } return $arr; } /** * Delete one account, deletes also all acl-entries for that account * * @param int $id numeric account_id * @return boolean true on success, false otherwise */ function delete($account_id) { if (!(int)$account_id) return false; if ($account_id < 0) { $sri = ldap_search($this->ds, $this->group_context, 'gidnumber=' . abs($account_id)); } else { // remove the user's memberships $this->set_memberships(array(),$account_id); $sri = ldap_search($this->ds, $this->user_context, 'uidnumber=' . $account_id); } if (!$sri) return false; $allValues = ldap_get_entries($this->ds, $sri); if (!$allValues['count']) return false; return ldap_delete($this->ds, $allValues[0]['dn']); } /** * Reads the data of one group * * @internal * @param int $account_id numeric account-id (< 0 as it's for a group) * @return array/boolean array with account data (keys: account_id, account_lid, ...) or false if account not found */ function _read_group($account_id) { $sri = ldap_search($this->ds, $this->group_context, 'gidnumber=' . abs($account_id), array('dn','gidnumber','cn')); $data = ldap_get_entries($this->ds, $sri); if (!$data['count']) { return false; // group not found } $data = $this->translation->convert($data[0],'utf-8'); return array( 'account_dn' => $data['dn'], 'account_id' => -$data['gidnumber'][0], 'account_lid' => $data['cn'][0], 'account_type' => 'g', 'account_firstname' => $data['cn'][0], 'account_lastname' => lang('Group'), ); } /** * Reads the data of one user * * @internal * @param int $account_id numeric account-id * @return array/boolean array with account data (keys: account_id, account_lid, ...) or false if account not found */ function _read_user($account_id) { $sri = ldap_search($this->ds, $this->user_context, 'uidnumber=' . $account_id, array('dn','uidnumber','uid','gidnumber','givenname','sn','cn','mail','userpassword', 'shadowexpire','shadowlastchange','homedirectory','loginshell')); $data = ldap_get_entries($this->ds, $sri); if (!$data['count']) { return false; // user not found } $data = $this->translation->convert($data[0],'utf-8'); $user = array( 'account_dn' => $data['dn'], 'account_id' => (int)$data['uidnumber'][0], 'account_lid' => $data['uid'][0], 'account_type' => 'u', 'account_primary_group' => -$data['gidnumber'][0], 'account_firstname' => $data['givenname'][0], 'account_lastname' => $data['sn'][0], 'account_email' => $data['mail'][0], 'account_fullname' => $data['cn'][0], 'account_pwd' => $data['userpassword'][0], // both status and expires are encoded in the single shadowexpire value in LDAP // - if it's unset an account is enabled AND does never expire // - if it's set to 0, the account is disabled // - if it's set to > 0, it will or already has expired --> acount is active if it not yet expired // shadowexpire is in days since 1970/01/01 (equivalent to a timestamp / (24*60*60) 'account_status' => isset($data['shadowexpire']) && $data['shadowexpire'][0]*24*3600 < time() ? false : 'A', 'account_expires' => isset($data['shadowexpire']) && $data['shadowexpire'][0] ? $data['shadowexpire'][0]*24*3600 : -1, 'account_lastpasswd_change' => isset($data['shadowlastchange']) ? $data['shadowlastchange'][0]*24*3600 : null, // lastlogin and lastlogin from are not availible via the shadowAccount object class // 'account_lastlogin' => $data['phpgwaccountlastlogin'][0], // 'account_lastloginfrom' => $data['phpgwaccountlastloginfrom'][0], ); if ($GLOBALS['egw_info']['server']['ldap_extra_attributes']) { $user['homedirectory'] = $data['homedirectory'][0]; $user['loginshell'] = $data['loginshell'][0]; } return $user; } /** * Merges the group releavant account data from $data into $to_write * * @internal * @param array $to_write data to write to ldap incl. objectclass ($data is NOT yet merged) * @param array $data array with account-data in utf-8 * @return array merged data */ function _merge_group($to_write,$data) { $to_write['gidnumber'] = abs($data['account_id']); $to_write['cn'] = $data['account_lid']; return $to_write; } /** * Merges the user releavant account data from $data into $to_write * * @internal * @param array $to_write data to write to ldap incl. objectclass ($data is NOT yet merged) * @param array $data array with account-data in utf-8 * @param boolean $new_entry * @return array merged data */ function _merge_user($to_write,$data,$new_entry) { //echo "

accounts_ldap::_merge_user(".print_r($to_write,true).','.print_r($data,true).",$new_entry)

\n"; $to_write['uidnumber'] = $data['account_id']; $to_write['uid'] = $data['account_lid']; $to_write['gidnumber'] = abs($data['account_primary_group']); if (!$new_entry || $data['account_firstname']) { $to_write['givenname'] = $data['account_firstname'] ? $data['account_firstname'] : array(); } $to_write['sn'] = $data['account_lastname']; if (!$new_entry || $data['account_email']) { $to_write['mail'] = $data['account_email'] ? $data['account_email'] : array(); } $to_write['cn'] = $data['account_fullname'] ? $data['account_fullname'] : $data['account_firstname'].' '.$data['account_lastname']; if (isset($data['account_passwd']) && $data['account_passwd']) { if(!@is_object($GLOBALS['egw']->auth)) { $GLOBALS['egw']->auth =& CreateObject('phpgwapi.auth'); } if (!preg_match('/^\\{[a-z5]{3,5}\\}.+/i',$data['account_passwd'])) // if it's not already entcrypted, do so now { $data['account_passwd'] = $GLOBALS['egw']->auth->encrypt_ldap($data['account_passwd']); } $to_write['userpassword'] = $data['account_passwd']; } // both status and expires are encoded in the single shadowexpire value in LDAP // - if it's unset an account is enabled AND does never expire // - if it's set to 0, the account is disabled // - if it's set to > 0, it will or already has expired --> acount is active if it not yet expired // shadowexpire is in days since 1970/01/01 (equivalent to a timestamp / (24*60*60) $to_write['shadowexpire'] = !$data['account_status'] ? ($data['account_expires'] != -1 && $data['account_expires'] < time() ? $data['account_expires']/(24*3600) : 0) : ($data['account_expires'] != -1 ? $data['account_expires']/(24*3600) : array()); // array() = unset value if ($new_entry && is_array($to_write['shadowexpire']) && !count($to_write['shadowexpire'])) { unset($to_write['shadowexpire']); // gives protocoll error otherwise } if ($data['account_lastpasswd_change']) $to_write['shadowlastchange'] = $data['lastpasswd_change']/(24*3600); // lastlogin and lastlogin from are not availible via the shadowAccount object class // $to_write['phpgwaccountlastlogin'] = $data['lastlogin']; // $to_write['phpgwaccountlastloginfrom'] = $data['lastloginfrom']; if ($GLOBALS['egw_info']['server']['ldap_extra_attributes']) { if (isset($data['homedirectory'])) $to_write['homedirectory'] = $data['homedirectory']; if (isset($data['loginshell'])) $to_write['loginshell'] = $data['loginshell'] ? $data['loginshell'] : array(); } if ($new_entry && !isset($to_write['homedirectory'])) { $to_write['homedirectory'] = '/dev/null'; // is a required attribute of posixAccount } return $to_write; } /** * Searches users and/or groups * * ToDo: implement a search like accounts::search * * @param string $_type * @param int $start=null * @param string $sort='' * @param string $order='' * @param string $query * @param int $offset=null * @param string $query_type * @return array */ function get_list($_type='both', $start = '',$sort = '', $order = '', $query = '', $offset = null, $query_type='') { //print "\$_type=$_type, \$start=$start , \$sort=$sort, \$order=$order, \$query=$query, \$offset=$offset, \$query_type=$query_type
"; $query = strtolower($query); if($_type != 'groups') { $filter = "(&(uidnumber=*)(objectclass=posixaccount)"; if (!empty($query) && $query != '*') { switch($query_type) { case 'all': default: $query = '*'.$query; // fall-through case 'start': $query .= '*'; // fall-through case 'exact': $filter .= "(|(uid=$query)(sn=$query)(cn=$query)(givenname=$query)(mail=$query))"; break; case 'firstname': case 'lastname': case 'lid': case 'email': $to_ldap = array( 'firstname' => 'givenname', 'lastname' => 'sn', 'lid' => 'uid', 'email' => 'mail', ); $filter .= '('.$to_ldap[$query_type].'=*'.$query.'*)'; break; } } $filter .= ')'; $sri = ldap_search($this->ds, $this->user_context, $filter); $allValues = ldap_get_entries($this->ds, $sri); while (list($null,$allVals) = @each($allValues)) { settype($allVals,'array'); $test = @$allVals['uid'][0]; if (!$GLOBALS['egw_info']['server']['global_denied_users'][$test] && $allVals['uid'][0]) { $accounts[] = Array( 'account_id' => $allVals['uidnumber'][0], 'account_lid' => $GLOBALS['egw']->translation->convert($allVals['uid'][0],'utf-8'), 'account_type' => 'u', 'account_firstname' => $GLOBALS['egw']->translation->convert($allVals['givenname'][0],'utf-8'), 'account_lastname' => $GLOBALS['egw']->translation->convert($allVals['sn'][0],'utf-8'), 'account_status' => isset($allVals['shadowexpire'][0]) && $allVals['shadowexpire'][0]/(24*3600) < time() ? false : 'A', 'account_email' => $allVals['mail'][0], ); } } } if ($_type != 'accounts') { if(empty($query) || $query == '*') { $filter = '(&(gidnumber=*)(objectclass=posixgroup))'; } else { $filter = "(&(gidnumber=*)(objectclass=posixgroup)(|(cn=*$query*)))"; } $sri = ldap_search($this->ds, $this->group_context, $filter); $allValues = ldap_get_entries($this->ds, $sri); while (list($null,$allVals) = @each($allValues)) { settype($allVals,'array'); $test = $allVals['cn'][0]; if (!$GLOBALS['egw_info']['server']['global_denied_groups'][$test] && $allVals['cn'][0]) { $accounts[] = Array( 'account_id' => -$allVals['gidnumber'][0], 'account_lid' => $GLOBALS['egw']->translation->convert($allVals['cn'][0],'utf-8'), 'account_type' => 'g', 'account_firstname' => $GLOBALS['egw']->translation->convert($allVals['cn'][0],'utf-8'), 'account_lastname' => lang('Group'), 'account_status' => 'A', ); } } } // sort the array $arrayFunctions =& CreateObject('phpgwapi.arrayfunctions'); if(empty($order)) { $order = 'account_lid'; } $sortedAccounts = $arrayFunctions->arfsort($accounts,explode(',',$order),$sort); $this->total = count($accounts); // return only the wanted accounts if (is_array($sortedAccounts)) { reset($sortedAccounts); if(is_numeric($start) && is_numeric($offset)) { return array_slice($sortedAccounts, $start, $offset); } elseif(is_numeric($start)) { return array_slice($sortedAccounts, $start, $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']); } else { return $sortedAccounts; } } return False; } /** * convert an alphanumeric account-value (account_lid, account_email) to the account_id * * Please note: * - if a group and an user have the same account_lid the group will be returned (LDAP only) * - if multiple user have the same email address, the returned user is undefined * * @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) { $name = $this->translation->convert($name,$this->translation->charset(),'utf-8'); if ($which == 'account_lid' && $account_type !== 'u') // groups only support account_lid { $sri = ldap_search($this->ds, $this->group_context, '(&(cn=' . $name . ')(objectclass=posixgroup))'); $allValues = ldap_get_entries($this->ds, $sri); if (@$allValues[0]['gidnumber'][0]) { return -$allValues[0]['gidnumber'][0]; } } $to_ldap = array( 'account_lid' => 'uid', 'account_email' => 'mail', 'account_fullname' => 'cn', ); if (!isset($to_ldap[$which]) || $account_type === 'g') return False; $sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))'); $allValues = ldap_get_entries($this->ds, $sri); if (@$allValues[0]['uidnumber'][0]) { return (int)$allValues[0]['uidnumber'][0]; } return False; } /** * Update the last login timestamps and the IP * * @param int $account_id * @param string $ip * @return int lastlogin time */ function update_lastlogin($_account_id, $ip) { return false; // not longer supported $entry['phpgwaccountlastlogin'] = time(); $entry['phpgwaccountlastloginfrom'] = $ip; $sri = ldap_search($this->ds, $GLOBALS['egw_info']['server']['ldap_context'], 'uidnumber=' . (int)$_account_id); $allValues = ldap_get_entries($this->ds, $sri); $dn = $allValues[0]['dn']; @ldap_modify($this->ds, $dn, $entry); return $allValues[0]['phpgwaccountlastlogin'][0]; } /** * Query memberships of a given account * * @param int $account_id * @return array/boolean array with account_id => account_lid pairs or false if account not found */ function memberships($account_id) { if (!(int) $account_id || !($account_lid = $this->id2name($account_id))) return false; $sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(memberuid=$account_lid))",array('cn','gidnumber')); $memberships = array(); foreach(ldap_get_entries($this->ds, $sri) as $key => $data) { if ($key === 'count') continue; $memberships[(string) -$data['gidnumber'][0]] = $data['cn'][0]; } //echo "accounts::memberships($account_id)"; _debug_array($memberships); return $memberships; } /** * Query the members of a group * * @param int $gid * @return array with uidnumber => uid pairs */ function members($gid) { if (!is_numeric($gid)) return false; $gid = abs($gid); // our gid is negative! $sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(gidnumber=$gid))",array('memberuid')); $group = ldap_get_entries($this->ds, $sri); $members = array(); if (isset($group[0]['memberuid'])) { foreach($group[0]['memberuid'] as $lid) { if (($id = $this->name2id($lid))) { $members[$id] = $lid; } } } //echo "accounts_ldap::members($gid)"; _debug_array($members); return $members; } /** * Sets the memberships of the given account * * @param array $groups array with gidnumbers * @param int $accountid uidnumber */ function set_memberships($groups,$accountid) { //echo "

accounts_ldap::set_memberships(".print_r($groups,true).",$accountid)

\n"; // remove not longer existing memberships if (($old_memberships = $this->memberships($accountid))) { $old_memberships = array_keys($old_memberships); foreach(array_diff($old_memberships,$groups) as $gid) { if (($members = $this->members($gid))) { unset($members[$this->account_id]); $this->set_members($members,$gid); } } } // adding new memberships foreach($old_memberships ? array_diff($groups,$old_memberships) : $groups as $gid) { $members = $this->members($gid); $members[$accountid] = $this->id2name($accountid); $this->set_members($members,$gid); } } /** * Set the members of a group * * @param array $members array with uidnumber or uid's * @param int $gid gidnumber of group to set */ function set_members($members,$gid) { //echo "

accounts_ldap::set_members(".print_r($members,true).",$gid)

\n"; if (!($cn = $this->id2name($gid))) return false; foreach($members as $key => $member) { if (is_numeric($member) && ($member = $this->id2name($member))) { $members[$key] = $member; } } if (!ldap_modify($this->ds,'cn='.$cn.','.$this->group_context,array('memberUid' => array_values(array_unique($members))))) { echo "ldap_modify(,'cn=$cn,$this->group_context',array('memberUid' => ".print_r(array_values(array_unique($members)),true)."))\n"; } } /** * Using the common functions next_id and last_id, find the next available account_id * * @internal * @param $string $account_type='u' (optional, default to 'u') * @return int/boolean interger account_id or false if none is free anymore */ function _get_nextid($account_type='u') { $min = $GLOBALS['egw_info']['server']['account_min_id'] ? $GLOBALS['egw_info']['server']['account_min_id'] : 0; $max = $GLOBALS['egw_info']['server']['account_max_id'] ? $GLOBALS['egw_info']['server']['account_max_id'] : 0; if ($account_type == 'g') { $type = 'groups'; $sign = -1; } else { $type = 'accounts'; $sign = 1; } /* Loop until we find a free id */ do { $account_id = (int) $GLOBALS['egw']->common->next_id($type,$min,$max); } while ($account_id && $this->exists($sign * $account_id)); // check need to include the sign! if (!$account_id || $GLOBALS['egw_info']['server']['account_max_id'] && $account_id > $GLOBALS['egw_info']['server']['account_max_id']) { return False; } return $account_id; } }