forked from extern/egroupware
34ec8e6cb3
- Fix some accounts backend were not returning results indexed by account ID
1855 lines
62 KiB
PHP
1855 lines
62 KiB
PHP
<?php
|
|
/**
|
|
* API - accounts active directory backend
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @author Ralf Becker <rb@stylite.de>
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package api
|
|
* @subpackage accounts
|
|
*/
|
|
|
|
namespace EGroupware\Api\Accounts;
|
|
|
|
use EGroupware\Api;
|
|
use EGroupware\Api\Ldap\ServerInfo;
|
|
|
|
require_once EGW_INCLUDE_ROOT.'/vendor/adldap2/adldap2/src/adLDAP.php';
|
|
use adLDAPException;
|
|
|
|
/**
|
|
* Active directory backend for accounts
|
|
*
|
|
* RID (relative id / last part of string-SID) is used as numeric account-id (negativ for groups).
|
|
* SID for queries get reconstructed from account_id by prepending domain-SID.
|
|
*
|
|
* Easiest way to enable SSL on a win2008r2 DC is to install role "Active Director Certificate Services"
|
|
* or in German "Active Directory-Zertifikatsdienste" AND reboot.
|
|
*
|
|
* Changing passwords require ldap_modify_batch method available in PHP 5.4 >= 5.4.26,
|
|
* PHP 5.5 >= 5.5.10 or PHP 5.6+. In earlier PHP versions ads_admin user (configured in setup)
|
|
* has to have "Reset Password" privileges!
|
|
*
|
|
* @access internal only use the interface provided by the accounts class
|
|
* @link http://www.selfadsi.org/user-attributes-w2k8.htm
|
|
* @link http://www.selfadsi.org/attributes-e2k7.htm
|
|
* @link http://msdn.microsoft.com/en-us/library/ms675090(v=vs.85).aspx
|
|
*/
|
|
class Ads
|
|
{
|
|
/**
|
|
* Instance of adLDAP class
|
|
*
|
|
* @var adLDAP
|
|
*/
|
|
private $adldap;
|
|
/**
|
|
* total number of found entries from get_list method
|
|
*
|
|
* @var int
|
|
*/
|
|
public $total;
|
|
|
|
/**
|
|
* Reference to our frontend
|
|
*
|
|
* @var Api\Accounts
|
|
*/
|
|
protected $frontend;
|
|
|
|
/**
|
|
* Value of expires attribute for never
|
|
*/
|
|
const EXPIRES_NEVER = '9223372036854775807';
|
|
|
|
/**
|
|
* AD does NOT allow to change sAMAccountName / account_lid
|
|
*/
|
|
const CHANGE_ACCOUNT_LID = false;
|
|
|
|
/**
|
|
* Backend requires password to be set, before allowing to enable an account
|
|
*/
|
|
const REQUIRE_PASSWORD_FOR_ENABLE = true;
|
|
|
|
/**
|
|
* Attributes to query to be able to generate account_id and account_lid
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $default_attributes = array(
|
|
'objectsid', 'samaccounttype', 'samaccountname',
|
|
);
|
|
|
|
/**
|
|
* Attributes to query for a user (need to contain $default_attributes!)
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $user_attributes = array(
|
|
'objectsid', 'samaccounttype', 'samaccountname',
|
|
'primarygroupid', 'givenname', 'sn', 'mail', 'displayname', 'telephonenumber',
|
|
'objectguid', 'useraccountcontrol', 'accountexpires', 'pwdlastset', 'whencreated', 'whenchanged', 'lastlogon',
|
|
);
|
|
|
|
/**
|
|
* Attributes to query for a group (need to contain $default_attributes!)
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $group_attributes = array(
|
|
'objectsid', 'samaccounttype', 'samaccountname',
|
|
'objectguid', 'mail', 'whencreated', 'whenchanged', 'description',
|
|
);
|
|
|
|
/**
|
|
* All users with an account_id below that get ignored, because they are system users (incl. 501="Administrator")
|
|
*/
|
|
const MIN_ACCOUNT_ID = 1000;
|
|
|
|
/**
|
|
* Ignore group-membership of following groups, when compiling group-members
|
|
*
|
|
* We ignore "Domain Users" group with RID 513, as it contains all users!
|
|
*
|
|
* @var int[]
|
|
*/
|
|
public $ignore_membership = [ -513 ];
|
|
/**
|
|
* Timestamps ldap => egw used in several places
|
|
*
|
|
* @var string[]
|
|
*/
|
|
public $timestamps2egw = [
|
|
'whencreated' => 'account_created',
|
|
'whenchanged' => 'account_modified',
|
|
'accountexpires' => 'account_expires',
|
|
'lastlogon' => 'account_lastlogin',
|
|
];
|
|
|
|
/**
|
|
* Other attributes sorted by their default matching rule
|
|
*/
|
|
public $other2egw = [
|
|
'primarygroupid' => 'account_primary_group',
|
|
];
|
|
|
|
/**
|
|
* String attributes which can be sorted by caseIgnoreMatch ldap => egw
|
|
*
|
|
* @var string[]
|
|
*/
|
|
public $attributes2egw = [
|
|
'samaccountname' => 'account_lid',
|
|
'sn' => 'account_lastname',
|
|
'givenname' => 'account_firstname',
|
|
'displayname' => 'account_fullname',
|
|
'mail' => 'account_email',
|
|
];
|
|
|
|
/**
|
|
* @var ServerInfo
|
|
*/
|
|
public $serverinfo;
|
|
|
|
/**
|
|
* Enable extra debug messages via error_log (error always get logged)
|
|
*/
|
|
public static $debug = false;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param Api\Accounts $frontend reference to the frontend class, to be able to call it's methods if needed
|
|
* @throws adLDAPException
|
|
*/
|
|
function __construct(Api\Accounts $frontend)
|
|
{
|
|
$this->frontend = $frontend;
|
|
|
|
$this->adldap = self::get_adldap($this->frontend->config);
|
|
|
|
$this->serverinfo = ServerInfo::get($this->ldap_connection(), $this->frontend->config['ads_host']);
|
|
}
|
|
|
|
/**
|
|
* Factory method and singelton to get adLDAP object for given configuration or default server config
|
|
*
|
|
* @param array $config=null values for keys 'ads_domain', 'ads_host' (required) and optional 'ads_admin_user', 'ads_admin_passwd', 'ads_connection'
|
|
* @return adLDAP
|
|
* @throws adLDAPException
|
|
*/
|
|
public static function get_adldap(array &$config=null)
|
|
{
|
|
static $adldap = array();
|
|
if (!$config) $config =& $GLOBALS['egw_info']['server'];
|
|
|
|
if (!isset($adldap[$config['ads_domain']]))
|
|
{
|
|
if (empty($config['ads_host'])) throw new Api\Exception("Required ADS host name(s) missing!");
|
|
if (empty($config['ads_domain'])) throw new Api\Exception("Required ADS domain missing!");
|
|
|
|
$base_dn_parts = array();
|
|
foreach(explode('.', $config['ads_domain']) as $dc)
|
|
{
|
|
$base_dn_parts[] = 'DC='.$dc;
|
|
}
|
|
$base_dn = implode(',', $base_dn_parts);
|
|
|
|
// check if a port is specified as host[:port] and pass it correctly to adLDAP
|
|
$matches = null;
|
|
if (preg_match('/:(\d+)/', $host=$config['ads_host'], $matches))
|
|
{
|
|
$port = $matches[1];
|
|
$host = preg_replace('/:(\d+)/', '', $config['ads_host']);
|
|
}
|
|
$options = array(
|
|
'domain_controllers' => preg_split('/[ ,]+/', $host),
|
|
'base_dn' => $base_dn ? $base_dn : null,
|
|
'account_suffix' => '@'.$config['ads_domain'],
|
|
'admin_username' => $config['ads_admin_user'],
|
|
'admin_password' => $config['ads_admin_passwd'],
|
|
'use_tls' => $config['ads_connection'] == 'tls',
|
|
'use_ssl' => $config['ads_connection'] == 'ssl',
|
|
'charset' => Api\Translation::charset(),
|
|
);
|
|
if (isset($port)) $options['ad_port'] = $port;
|
|
|
|
$adldap[$config['ads_domain']] = new adLDAP($options);
|
|
if (self::$debug) error_log(__METHOD__."() new adLDAP(".array2string($options).") returned ".array2string($adldap[$config['ads_domain']]).' '.function_backtrace());
|
|
}
|
|
//else error_log(__METHOD__."() returning cached adLDAP ".array2string($adldap[$config['ads_domain']]).' '.function_backtrace());
|
|
return $adldap[$config['ads_domain']];
|
|
}
|
|
|
|
/**
|
|
* Get SID of domain or an account
|
|
*
|
|
* @param int $account_id
|
|
* @return string|NULL
|
|
*/
|
|
protected function get_sid($account_id=null)
|
|
{
|
|
static $domain_sid = null;
|
|
if (!isset($domain_sid))
|
|
{
|
|
$domain_sid = Api\Cache::getCache($this->frontend->config['install_id'], __CLASS__, 'ads_domain_sid');
|
|
if ((!is_array($domain_sid) || !isset($domain_sid[$this->frontend->config['ads_domain']])) &&
|
|
($adldap = self::get_adldap($this->frontend->config)) &&
|
|
($sr = ldap_search($adldap->getLdapConnection(), $adldap->getBaseDn(), '(objectclass=domain)', array('objectsid'))) &&
|
|
(($entries = ldap_get_entries($adldap->getLdapConnection(), $sr)) || true))
|
|
{
|
|
$domain_sid = array();
|
|
$domain_sid[$this->frontend->config['ads_domain']] = $adldap->utilities()->getTextSID($entries[0]['objectsid'][0]);
|
|
Api\Cache::setCache($this->frontend->config['install_id'], __CLASS__, 'ads_domain_sid', $domain_sid);
|
|
}
|
|
}
|
|
$sid = $domain_sid[$this->frontend->config['ads_domain']];
|
|
if ($sid && abs($account_id))
|
|
{
|
|
$sid .= '-'.abs($account_id);
|
|
}
|
|
return $sid;
|
|
}
|
|
|
|
const DOMAIN_USERS_GROUP = 513;
|
|
const ADS_CONTEXT = 'ads_context';
|
|
|
|
/**
|
|
* Get context for user and group objects
|
|
*
|
|
* Can be set via server-config "ads_context", otherwise baseDN is used
|
|
*
|
|
* @param boolean $set_if_empty =false true set from DN of "Domain Users" group #
|
|
* @return string
|
|
*/
|
|
public function ads_context($set_if_empty=false)
|
|
{
|
|
if (empty($this->frontend->config[self::ADS_CONTEXT]))
|
|
{
|
|
if ($set_if_empty && ($dn = $this->id2name(-self::DOMAIN_USERS_GROUP, 'account_dn')))
|
|
{
|
|
$dn = preg_replace('/^CN=.*?,(CN|OU)=/i', '$1=', $dn);
|
|
Api\Config::save_value(self::ADS_CONTEXT, $this->frontend->config[self::ADS_CONTEXT]=$dn, 'phpgwapi');
|
|
}
|
|
else
|
|
{
|
|
return $this->adldap->getBaseDn();
|
|
}
|
|
}
|
|
return $this->frontend->config[self::ADS_CONTEXT];
|
|
}
|
|
|
|
/**
|
|
* Get container for new user and group objects
|
|
*
|
|
* Can be set via server-config "ads_context", otherwise parent of DN from "Domain Users" is used
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _get_container()
|
|
{
|
|
$context = $this->ads_context(true);
|
|
$base = $this->adldap->getBaseDn();
|
|
$matches = null;
|
|
if (!preg_match('/^(.*),'.preg_quote($base, '/').'$/i', $context, $matches))
|
|
{
|
|
throw new Api\Exception\WrongUserinput("Wrong or not configured ADS context '$context' (baseDN='$base')!");
|
|
}
|
|
$container = $matches[1];
|
|
if (self::$debug) error_log(__METHOD__."() context='$context', base='$base' returning ".array2string($container));
|
|
return $container;
|
|
}
|
|
|
|
/**
|
|
* Get connection to ldap server from adLDAP
|
|
*
|
|
* @param boolean $reconnect =false true: reconnect even if already connected
|
|
* @return resource
|
|
*/
|
|
public function ldap_connection($reconnect=false)
|
|
{
|
|
if (($reconnect || !($ds = $this->adldap->getLdapConnection())) &&
|
|
// call connect, thought I dont know how it can be not connected ...
|
|
!$this->adldap->connect() || !($ds = $this->adldap->getLdapConnection()))
|
|
{
|
|
error_log(__METHOD__."() !this->adldap->getLdapConnection() this->adldap=".array2string($this->adldap));
|
|
}
|
|
return $ds;
|
|
}
|
|
|
|
/**
|
|
* Get GUID from SID, as adLDAP only works on GUID not SID currently
|
|
*
|
|
* @param string $sid
|
|
* @return string|NULL
|
|
*/
|
|
/*protected function sid2guid($sid)
|
|
{
|
|
if (($sr = ldap_search($this->adldap->getLdapConnection(), $this->ads_context(), 'objectsid='.$sid, array('objectguid'))) &&
|
|
($entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr)))
|
|
{
|
|
return $this->adldap->utilities()->decodeGuid($entries[0]['objectguid'][0]);
|
|
}
|
|
return null;
|
|
}*/
|
|
|
|
/**
|
|
* Convert SID to account_id (RID = last part of SID)
|
|
*
|
|
* @param string $sid
|
|
* @return int
|
|
*/
|
|
public static function sid2account_id($sid)
|
|
{
|
|
$parts = explode('-', $sid);
|
|
|
|
return (int)array_pop($parts);
|
|
}
|
|
|
|
/**
|
|
* Convert binary SID to account_id (RID = last part of SID)
|
|
*
|
|
* @param string $objectsid
|
|
* @return int
|
|
*/
|
|
public function objectsid2account_id($objectsid)
|
|
{
|
|
$sid = $this->adldap->utilities()->getTextSID(is_array($objectsid) ? $objectsid[0] : $objectsid);
|
|
|
|
return self::sid2account_id($sid);
|
|
}
|
|
|
|
/**
|
|
* Convert binary GUID to string
|
|
*
|
|
* @param string $objectguid
|
|
* @return int
|
|
*/
|
|
public function objectguid2str($objectguid)
|
|
{
|
|
return $this->adldap->utilities()->decodeGuid(is_array($objectguid) ? $objectguid[0] : $objectguid);
|
|
}
|
|
|
|
/**
|
|
* Convert a string GUID to hex string used in filter
|
|
*
|
|
* @param string $strGUID
|
|
* @return int
|
|
*/
|
|
public function objectguid2hex($strGUID)
|
|
{
|
|
return $this->adldap->utilities()->strGuidToHex($strGUID);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public function read($account_id)
|
|
{
|
|
if (!(int)$account_id) return false;
|
|
|
|
$ret = $account_id < 0 ? $this->_read_group($account_id) : $this->_read_user($account_id);
|
|
if (self::$debug) error_log(__METHOD__."($account_id) returning ".array2string($ret));
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* 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 = Api\Translation::convert($data, Api\Translation::charset(), 'utf-8');
|
|
|
|
if ($data['account_id'] && !($old = $this->read($data['account_id'])))
|
|
{
|
|
error_log(__METHOD__.'('.array2string($data).") account NOT found!");
|
|
return false;
|
|
}
|
|
if ($old)
|
|
{
|
|
if (($old['account_type'] == 'g') != $is_group)
|
|
{
|
|
error_log(__METHOD__.'('.array2string($data).") changing account-type user <--> group forbidden!");
|
|
return false;
|
|
}
|
|
$old = Api\Translation::convert($old, Api\Translation::charset(), 'utf-8');
|
|
}
|
|
$ret = $is_group ? $this->_save_group($data, $old) : $this->_save_user($data, $old);
|
|
|
|
if (self::$debug) error_log(__METHOD__.'('.array2string($data).') returning '.array2string($ret));
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Delete one account, deletes also all acl-entries for that account
|
|
*
|
|
* @param int $account_id numeric account_id
|
|
* @return boolean true on success, false otherwise
|
|
*/
|
|
function delete($account_id)
|
|
{
|
|
if (!(int)$account_id || !($account_lid = $this->id2name($account_id)))
|
|
{
|
|
error_log(__METHOD__."($account_id) NOT found!");
|
|
return false;
|
|
}
|
|
|
|
// for some reason deleting fails with "ldap_search(): supplied argument is not a valid ldap link resource"
|
|
// forcing a reconnect fixes it ;-)
|
|
$this->ldap_connection(true);
|
|
|
|
if ($account_id < 0)
|
|
{
|
|
$ret = $this->adldap->group()->delete($account_lid);
|
|
}
|
|
else
|
|
{
|
|
$ret = $this->adldap->user()->delete($account_lid);
|
|
}
|
|
if (self::$debug) error_log(__METHOD__."($account_id) account_lid='$account_lid' returning ".array2string($ret));
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Convert ldap data of a group
|
|
*
|
|
* @param array $_data
|
|
* @return array
|
|
*/
|
|
protected function _ldap2group($_data)
|
|
{
|
|
$data = Api\Translation::convert($_data, 'utf-8');
|
|
|
|
// no need to calculate sid, if already calculated
|
|
$sid = is_string($data['objectsid']) ? $data['objectsid'] :
|
|
$this->adldap->utilities()->getTextSID($data['objectsid'][0]);
|
|
$account_id = -self::sid2account_id($sid);
|
|
|
|
$group = array(
|
|
'account_dn' => $data['dn'],
|
|
'account_id' => $account_id,
|
|
'account_sid' => $sid,
|
|
'account_guid' => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]),
|
|
'account_lid' => $data['samaccountname'][0],
|
|
'account_type' => 'g',
|
|
'account_firstname' => $data['samaccountname'][0],
|
|
'account_lastname' => lang('Group'),
|
|
'account_fullname' => lang('Group').' '.$data['samaccountname'][0],
|
|
'account_email' => $data['mail'][0],
|
|
'account_created' => !isset($data['whencreated'][0]) ? null :
|
|
self::_when2ts($data['whencreated'][0]),
|
|
'account_modified' => !isset($data['whenchanged'][0]) ? null :
|
|
self::_when2ts($data['whenchanged'][0]),
|
|
'account_description' => $data['description'][0],
|
|
'mailAllowed' => true,
|
|
);
|
|
//error_log(__METHOD__."(".array2string($data).") returning ".array2string($group));
|
|
return $group;
|
|
}
|
|
|
|
/**
|
|
* Reads the data of one group
|
|
*
|
|
* @internal
|
|
* @todo take recursive group memberships into account
|
|
* @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
|
|
*/
|
|
protected function _read_group($account_id)
|
|
{
|
|
if (!($data = $this->filter(array('objectsid' => $this->get_sid($account_id)), 'g', self::$group_attributes)))
|
|
{
|
|
return false; // group not found
|
|
}
|
|
$group = $this->_ldap2group(array_shift($data));
|
|
|
|
// for memberships we have to query primaryGroupId and memberOf of users
|
|
$group['members'] = $this->filter(array('memberOf' => $group['account_dn']), 'u');
|
|
// primary group is not stored in memberOf attribute, need to add them too
|
|
$group['members'] = $this->filter(array('primaryGroupId' => abs($account_id)), 'u', null, $group['members']);
|
|
|
|
return $group;
|
|
}
|
|
|
|
/**
|
|
* Convert ldap data of a user
|
|
*
|
|
* @param array $_data
|
|
* @return array
|
|
*/
|
|
protected function _ldap2user(array $_data)
|
|
{
|
|
$data = Api\Translation::convert($_data, 'utf-8');
|
|
|
|
// no need to calculate sid, if already calculated
|
|
$sid = is_string($data['objectsid']) ? $data['objectsid'] :
|
|
$this->adldap->utilities()->getTextSID($data['objectsid'][0]);
|
|
$account_id = self::sid2account_id($sid);
|
|
|
|
$user = array(
|
|
'account_dn' => $data['dn'],
|
|
'account_id' => $account_id,
|
|
'account_sid' => $sid,
|
|
'account_guid' => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]),
|
|
'account_lid' => $data['samaccountname'][0],
|
|
'account_type' => 'u',
|
|
'account_primary_group' => (string)-$data['primarygroupid'][0],
|
|
'account_firstname' => $data['givenname'][0],
|
|
'account_lastname' => $data['sn'][0],
|
|
'account_email' => $data['mail'][0],
|
|
'account_fullname' => $data['displayname'][0],
|
|
'account_phone' => $data['telephonenumber'][0],
|
|
'account_status' => $data['useraccountcontrol'][0] & 2 ? false : 'A',
|
|
'account_expires' => !isset($data['accountexpires']) || !$data['accountexpires'][0] ||
|
|
$data['accountexpires'][0] == self::EXPIRES_NEVER ? -1 :
|
|
$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['accountexpires'][0]),
|
|
'account_lastpwd_change' => !isset($data['pwdlastset']) ? null : (!$data['pwdlastset'][0] ? 0 :
|
|
$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['pwdlastset'][0])),
|
|
'account_lastlogin' => empty($data['lastlogon'][0]) ? null :
|
|
$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['lastlogon'][0]),
|
|
'account_created' => !isset($data['whencreated'][0]) ? null :
|
|
self::_when2ts($data['whencreated'][0]),
|
|
'account_modified' => !isset($data['whenchanged'][0]) ? null :
|
|
self::_when2ts($data['whenchanged'][0]),
|
|
);
|
|
// expired accounts are NOT active
|
|
if ($user['account_expires'] !== -1 && $user['account_expires'] < time())
|
|
{
|
|
$user['account_status'] = false;
|
|
}
|
|
$user['person_id'] = $user['account_guid']; // id of contact
|
|
//error_log(__METHOD__."(".array2string($data).") returning ".array2string($user));
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Check if user is active
|
|
*
|
|
* @param array $data values for attributes 'useraccountcontrol' and 'accountexpires'
|
|
* @return boolean true if user is active, false otherwise
|
|
*/
|
|
public function user_active(array $data)
|
|
{
|
|
$user = $this->_ldap2user($data);
|
|
$active = Api\Accounts::is_active($user);
|
|
//error_log(__METHOD__."(cn={$data['cn'][0]}, useraccountcontrol={$data['useraccountcontrol'][0]}, accountexpires={$data['accountexpires'][0]}) user=".array2string($user)." returning ".array2string($active));
|
|
return $active;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
protected function _read_user($account_id)
|
|
{
|
|
if (!($data = $this->filter(array('objectsid' => $this->get_sid($account_id)), 'u', self::$user_attributes)))
|
|
{
|
|
return false; // user not found
|
|
}
|
|
$user = $this->_ldap2user(array_shift($data));
|
|
|
|
// query memberships direct, as accounts class will query it anyway and we still have dn and primary group available
|
|
$user['memberships'] = $this->filter(array('member' => $user['account_dn']), 'g');
|
|
if (!isset($user['memberships'][$user['account_primary_group']]))
|
|
{
|
|
$user['memberships'][$user['account_primary_group']] = $this->id2name($user['account_primary_group']);
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
const WHEN_FORMAT = 'YmdHis';
|
|
|
|
/**
|
|
* Convert when(Created|Changed) attribute to unix timestamp
|
|
*
|
|
* @param string $_when eg. "20130520200000.0Z"
|
|
* @return int
|
|
*/
|
|
protected static function _when2ts($_when)
|
|
{
|
|
static $utc=null;
|
|
if (!isset($utc)) $utc = new \DateTimeZone('UTC');
|
|
|
|
list($when) = explode('.', $_when); // remove .0Z not understood by createFromFormat
|
|
$datetime = Api\DateTime::createFromFormat(self::WHEN_FORMAT, $when, $utc);
|
|
if (Api\DateTime::$server_timezone) $datetime->setTimezone(Api\DateTime::$server_timezone);
|
|
|
|
return $datetime->getTimestamp();
|
|
}
|
|
|
|
/**
|
|
* Saves a group
|
|
*
|
|
* @internal
|
|
* @param array $data array with account-data in utf-8
|
|
* @param array $old =null current data
|
|
* @return int|false account_id or false on error
|
|
*/
|
|
protected function _save_group(array &$data, array $old=null)
|
|
{
|
|
//error_log(__METHOD__.'('.array2string($data).', old='.array2string($old).')');
|
|
|
|
if (!$old) // new entry
|
|
{
|
|
static $new2adldap = array(
|
|
'account_lid' => 'group_name',
|
|
'account_description' => 'description',
|
|
);
|
|
$attributes = array();
|
|
foreach($new2adldap as $egw => $adldap)
|
|
{
|
|
$attributes[$adldap] = (string)$data[$egw];
|
|
}
|
|
$attributes['container'] = $this->_get_container();
|
|
|
|
$ret = $this->adldap->group()->create($attributes);
|
|
if ($ret !== true)
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") adldap->group()->create(".array2string($attributes).') returned '.array2string($ret));
|
|
return false;
|
|
}
|
|
if (!($ret = $this->name2id($data['account_lid'])) || !($old = $this->read($ret)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") newly created group NOT found!");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Samba4 does NOT allow to change samaccountname, but CN or DN of a group!
|
|
// therefore we do NOT allow to change group-name for now (adLDAP also has no method for it)
|
|
/* check if DN/account_lid changed (not yet supported by adLDAP)
|
|
if ($old['account_lid'] !== $data['account_lid'])
|
|
{
|
|
if (!($ret = ldap_rename($ds=$this->ldap_connection(), $old['account_dn'],
|
|
'CN='.$this->adldap->utilities()->ldapSlashes($data['account_lid']), null, true)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") rename to new CN failed!");
|
|
return false;
|
|
}
|
|
}*/
|
|
static $egw2adldap = array(
|
|
//'account_lid' => 'samaccountname', // need to be changed too
|
|
'account_email' => 'mail',
|
|
'account_description' => 'description',
|
|
);
|
|
$ldap = array();
|
|
foreach($egw2adldap as $egw => $adldap)
|
|
{
|
|
if (isset($data[$egw]) && (string)$data[$egw] != (string)$old[$egw])
|
|
{
|
|
switch($egw)
|
|
{
|
|
case 'account_description':
|
|
$ldap[$adldap] = !empty($data[$egw]) ? $data[$egw] : array();
|
|
break;
|
|
|
|
default:
|
|
$ldap[$adldap] = $data[$egw];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// attributes not (yet) suppored by adldap
|
|
if ($ldap && !($ret = @ldap_modify($ds=$this->ldap_connection(), $old['account_dn'], $ldap)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") ldap_modify($ds, '$old[account_dn]', ".array2string($ldap).') returned '.array2string($ret));
|
|
return false;
|
|
}
|
|
return $old['account_id'];
|
|
}
|
|
|
|
/**
|
|
* Saves a user account
|
|
*
|
|
* @internal
|
|
* @param array $data array with account-data in utf-8
|
|
* @param array $old =null current data
|
|
* @return int|false account_id or false on error
|
|
*/
|
|
protected function _save_user(array &$data, array $old=null)
|
|
{
|
|
//error_log(__METHOD__.'('.array2string($data).', old='.array2string($old).')');
|
|
if (!isset($data['account_fullname']) && !empty($data['account_firstname']) && !empty($data['account_lastname']))
|
|
{
|
|
$data['account_fullname'] = $data['account_firstname'].' '.$data['account_lastname'];
|
|
}
|
|
|
|
if (($new_entry = !$old)) // new entry
|
|
{
|
|
static $new2adldap = array(
|
|
'account_lid' => 'username',
|
|
'account_firstname' => 'firstname',
|
|
'account_lastname' => 'surname',
|
|
'account_email' => 'email',
|
|
'account_fullname' => 'display_name',
|
|
'account_passwd' => 'password',
|
|
'account_status' => 'enabled',
|
|
);
|
|
$attributes = array();
|
|
foreach($new2adldap as $egw => $adldap)
|
|
{
|
|
if ($egw == 'account_passwd' && (empty($data[$egw]) ||
|
|
!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()))
|
|
{
|
|
continue; // do not try to set password, if no SSL or TLS, whole user creation will fail
|
|
}
|
|
if (isset($data[$egw])) $attributes[$adldap] = $data[$egw];
|
|
}
|
|
$attributes['enabled'] = !isset($data['account_status']) || $data['account_status'] === 'A';
|
|
$attributes['container'] = $this->_get_container();
|
|
|
|
$ret = $this->adldap->user()->create($attributes);
|
|
if ($ret !== true)
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") adldap->user()->create(".array2string($attributes).') returned '.array2string($ret));
|
|
return false;
|
|
}
|
|
if (!($ret = $this->name2id($data['account_lid'])) || !($old = $this->read($ret)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") newly created user NOT found!");
|
|
return false;
|
|
}
|
|
$data['account_id'] = $old['account_id'];
|
|
}
|
|
// check if DN/account_lid changed (not yet supported by adLDAP)
|
|
/* disabled as AD does NOT allow to change user-name (account_lid), which is used for DN
|
|
if (isset($data['account_lid']) && $old['account_lid'] !== $data['account_lid'] ||
|
|
(stripos($old['account_dn'], 'CN='.$data['account_lid'].',') !== 0))
|
|
{
|
|
if (!($ret = ldap_rename($ds=$this->ldap_connection(), $old['account_dn'],
|
|
'CN='.$this->adldap->utilities()->ldapSlashes($data['account_lid']), null, true)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") rename to new CN failed!");
|
|
return false;
|
|
}
|
|
}*/
|
|
static $egw2adldap = array(
|
|
'account_lid' => 'samaccountname',
|
|
'account_firstname' => 'firstname',
|
|
'account_lastname' => 'surname',
|
|
'account_email' => 'email',
|
|
'account_fullname' => 'display_name', // handeled currently in rename above, as not supported by adLDAP
|
|
'account_passwd' => 'password',
|
|
'account_status' => 'enabled',
|
|
'account_primary_group' => 'primarygroupid',
|
|
'account_expires' => 'expires',
|
|
//'mustchangepassword'=> 'change_password', // can only set it, but not reset it, therefore we set pwdlastset direct
|
|
'account_lastpwd_change' => 'pwdlastset',
|
|
//'account_phone' => 'telephone', not updated by accounts, only read so far
|
|
);
|
|
$attributes = $ldap = array();
|
|
// for a new entry set certain values (eg. profilePath) to in setup configured value
|
|
if ($new_entry)
|
|
{
|
|
foreach($this->frontend->config as $name => $value)
|
|
{
|
|
if (substr($name, 0, 8) == 'ads_new_')
|
|
{
|
|
$ldap[substr($name, 8)] = str_replace('%u', $data['account_lid'], $value);
|
|
}
|
|
}
|
|
}
|
|
foreach($egw2adldap as $egw => $adldap)
|
|
{
|
|
if (isset($data[$egw]) && (string)$data[$egw] != (string)$old[$egw])
|
|
{
|
|
switch($egw)
|
|
{
|
|
case 'account_passwd':
|
|
if (!empty($data[$egw]) && ($this->adldap->getUseSSL() || $this->adldap->getUseTLS()))
|
|
{
|
|
$attributes[$adldap] = $data[$egw]; // only try to set password, if no SSL or TLS
|
|
}
|
|
break;
|
|
case 'account_primary_group':
|
|
// setting a primary group seems to fail, if user is no member of that group
|
|
if (isset($old['memberships'][$data[$egw]]) ||
|
|
($group=$this->id2name($data[$egw])) && $this->adldap->group()->addUser($group, $data['account_id']))
|
|
{
|
|
$old['memberships'][$data[$egw]] = $group;
|
|
$ldap[$adldap] = abs($data[$egw]);
|
|
}
|
|
break;
|
|
case 'account_lid':
|
|
$ldap[$adldap] = $data[$egw];
|
|
$ldap['userPrincipalName'] = $data[$egw].'@'.$this->frontend->config['ads_domain'];
|
|
break;
|
|
case 'account_expires':
|
|
$attributes[$adldap] = $data[$egw] == -1 ? self::EXPIRES_NEVER :
|
|
self::convertUnixTimeToWindowsTime($data[$egw]);
|
|
break;
|
|
case 'account_status':
|
|
if ($new_entry && empty($data['account_passwd'])) continue 2; // cant active new account without passwd!
|
|
$attributes[$adldap] = $data[$egw] == 'A';
|
|
break;
|
|
case 'account_lastpwd_change':
|
|
// Samba4 does not understand -1 for current time, but Win2008r2 only allows to set -1 (beside 0)
|
|
// call Api\Auth\Ads::setLastPwdChange with true to get correct modification for both
|
|
$ldap = array_merge($ldap, Api\Auth\Ads::setLastPwdChange($data['account_lid'], null, $data[$egw], true));
|
|
break;
|
|
default:
|
|
$attributes[$adldap] = $data[$egw];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// check if we need to update something
|
|
if ($attributes && !($ret = $this->adldap->user()->modify($data['account_lid'], $attributes)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") adldap->user()->modify('$data[account_lid]', ".array2string($attributes).') returned '.array2string($ret).' '.function_backtrace());
|
|
return false;
|
|
}
|
|
//elseif ($attributes) error_log(__METHOD__."(".array2string($data).") adldap->user()->modify('$data[account_lid]', ".array2string($attributes).') returned '.array2string($ret).' '.function_backtrace());
|
|
// attributes not (yet) suppored by adldap
|
|
if ($ldap && !($ret = @ldap_modify($ds=$this->ldap_connection(), $old['account_dn'], $ldap)))
|
|
{
|
|
error_log(__METHOD__."(".array2string($data).") ldap_modify($ds, '$old[account_dn]', ".array2string($ldap).') returned '.array2string($ret).' ('.ldap_error($ds).') '.function_backtrace());
|
|
return false;
|
|
}
|
|
//elseif ($ldap) error_log(__METHOD__."(".array2string($data).") ldap_modify($ds, '$old[account_dn]', ".array2string($ldap).') returned '.array2string($ret).' '.function_backtrace());
|
|
|
|
//error_log(__METHOD__."(".array2string($data).") returning ".array2string($old['account_id']));
|
|
return $old['account_id'];
|
|
}
|
|
|
|
/**
|
|
* Add seconds between 1601-01-01 and 1970-01-01 and multiply by 10000000
|
|
*
|
|
* @param int $unixTime
|
|
* @return int windowsTime
|
|
*/
|
|
public static function convertUnixTimeToWindowsTime($unixTime)
|
|
{
|
|
return ($unixTime + 11644477200) * 10000000;
|
|
}
|
|
|
|
/**
|
|
* Searches / lists accounts: users and/or groups
|
|
*
|
|
* @param array with the following keys:
|
|
* @param $param['type'] string/int 'accounts', 'groups', 'owngroups' (groups the user is a member of), 'both'
|
|
* or integer group-id for a list of members of that group
|
|
* @param $param['start'] int first account to return (returns offset or max_matches entries) or all if not set
|
|
* @param $param['order'] string column to sort after, default account_lid if unset
|
|
* @param $param['sort'] string 'ASC' or 'DESC', default 'ASC' if not set
|
|
* @param $param['query'] string to search for, no search if unset or empty
|
|
* @param $param['query_type'] string:
|
|
* 'all' - query all fields for containing $param[query]
|
|
* 'start' - query all fields starting with $param[query]
|
|
* 'exact' - query all fields for exact $param[query]
|
|
* 'lid','firstname','lastname','email' - query only the given field for containing $param[query]
|
|
* @param $param['offset'] int - number of matches to return if start given, default use the value in the prefs
|
|
* @param $param['objectclass'] boolean return objectclass(es) under key 'objectclass' in each account
|
|
* @param $param['active'] boolean true: only return active / not expired accounts
|
|
* @return array with account_id => data pairs, data is an array with account_id, account_lid, account_firstname,
|
|
* account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group
|
|
*/
|
|
function search($param)
|
|
{
|
|
//error_log(__METHOD__.'('.json_encode($param).') '.function_backtrace());
|
|
$start = $param['start'];
|
|
if (!($maxmatchs = $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'])) $maxmatchs = 15;
|
|
if (!($offset = $param['offset'])) $offset = $maxmatchs;
|
|
|
|
$this->total = null;
|
|
$query = Api\Ldap::quote(strtolower($param['query']));
|
|
|
|
$accounts = array();
|
|
if($param['type'] !== 'groups')
|
|
{
|
|
if (!empty($query) && $query != '*')
|
|
{
|
|
switch($param['query_type'])
|
|
{
|
|
case 'all':
|
|
default:
|
|
$query = '*'.$query;
|
|
// fall-through
|
|
case 'start':
|
|
$query .= '*';
|
|
// fall-through
|
|
case 'exact':
|
|
$filter = "(|(samaccountname=$query)(sn=$query)(cn=$query)(givenname=$query)(mail=$query))";
|
|
break;
|
|
case 'firstname':
|
|
case 'lastname':
|
|
case 'lid':
|
|
case 'email':
|
|
static $to_ldap = array(
|
|
'firstname' => 'givenname',
|
|
'lastname' => 'sn',
|
|
'lid' => 'samaccountname',
|
|
'email' => 'mail',
|
|
);
|
|
$filter = '('.$to_ldap[$param['query_type']].'=*'.$query.'*)';
|
|
break;
|
|
}
|
|
}
|
|
if (is_numeric($param['type']))
|
|
{
|
|
$membership_filter = '(|(memberOf='.$this->id2name((int)$param['type'], 'account_dn').')(PrimaryGroupId='.abs($param['type']).'))';
|
|
$filter = $filter ? "(&$membership_filter$filter)" : $membership_filter;
|
|
}
|
|
foreach($this->filter($filter, 'u', self::$user_attributes, [], $param['active'], $param['order'].' '.$param['sort'], $start, $offset, $this->total) as $account_id => $data)
|
|
{
|
|
$account = $this->_ldap2user($data);
|
|
$account['account_fullname'] = Api\Accounts::format_username($account['account_lid'],$account['account_firstname'],$account['account_lastname'],$account['account_id']);
|
|
$accounts[$account_id] = $account;
|
|
}
|
|
}
|
|
if ($param['type'] === 'groups' || $param['type'] === 'both')
|
|
{
|
|
$query = Api\Ldap::quote(strtolower($param['query']));
|
|
|
|
$filter = null;
|
|
if(!empty($query) && $query != '*')
|
|
{
|
|
switch($param['query_type'])
|
|
{
|
|
case 'all':
|
|
default:
|
|
$query = '*'.$query;
|
|
// fall-through
|
|
case 'start':
|
|
$query .= '*';
|
|
// fall-through
|
|
case 'exact':
|
|
break;
|
|
}
|
|
$filter = "(|(cn=$query)(description=$query))";
|
|
}
|
|
foreach($this->filter($filter, 'g', self::$group_attributes) as $account_id => $data)
|
|
{
|
|
$accounts[$account_id] = $this->_ldap2group($data);
|
|
}
|
|
}
|
|
// sort the array
|
|
$this->_callback_sort = strtoupper($param['sort']);
|
|
$this->_callback_order = empty($param['order']) ? array('account_lid') : explode(',',$param['order']);
|
|
foreach($this->_callback_order as &$col)
|
|
{
|
|
if (substr($col, 0, 8) !== 'account_') $col = 'account_'.$col;
|
|
}
|
|
$sortedAccounts = $accounts;
|
|
uasort($sortedAccounts,array($this,'_sort_callback'));
|
|
|
|
$this->total = $this->total ?? count($accounts);
|
|
|
|
// return only the wanted accounts
|
|
reset($sortedAccounts);
|
|
if(is_numeric($start) && is_numeric($offset))
|
|
{
|
|
return array_slice($sortedAccounts, $start, $offset, true);
|
|
}
|
|
//error_log(__METHOD__.'('.array2string($param).') returning all '.array2string($sortedAccounts));
|
|
return $sortedAccounts;
|
|
}
|
|
|
|
/**
|
|
* DESC or ASC
|
|
*
|
|
* @var string
|
|
*/
|
|
private $_callback_sort = 'ASC';
|
|
/**
|
|
* column_names to sort by
|
|
*
|
|
* @var array
|
|
*/
|
|
private $_callback_order = array('account_lid');
|
|
|
|
/**
|
|
* Sort callback for uasort
|
|
*
|
|
* @param array $a
|
|
* @param array $b
|
|
* @return int
|
|
*/
|
|
protected function _sort_callback($a,$b)
|
|
{
|
|
foreach($this->_callback_order as $col )
|
|
{
|
|
if($this->_callback_sort != 'DESC')
|
|
{
|
|
$cmp = strcasecmp( $a[$col], $b[$col] );
|
|
}
|
|
else
|
|
{
|
|
$cmp = strcasecmp( $b[$col], $a[$col] );
|
|
}
|
|
if ( $cmp != 0 )
|
|
{
|
|
return $cmp;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get LDAP filter for user, groups or both
|
|
*
|
|
* @param string|null $account_type u = user, g = group, default null = try both
|
|
* @param bool $filter_expired =false true: filter out expired users
|
|
* @return string string with LDAP filter
|
|
*/
|
|
public function type_filter($account_type=null, $filter_expired=false)
|
|
{
|
|
switch ($account_type)
|
|
{
|
|
default: // user or groups
|
|
case 'u':
|
|
$type_filter = '(&(samaccounttype=' . adLDAP::ADLDAP_NORMAL_ACCOUNT . ')';
|
|
$type_filter .= '(!(isCriticalSystemObject=*))'; // exclude stock users (eg. Administrator) and groups
|
|
if ($filter_expired)
|
|
{
|
|
$type_filter .= '(|(!(accountExpires=*))(accountExpires=0)(accountExpires>='.self::convertUnixTimeToWindowsTime(time()).'))';
|
|
}
|
|
if (!empty($this->frontend->config['ads_user_filter']))
|
|
{
|
|
$type_filter .= $this->frontend->config['ads_user_filter'];
|
|
}
|
|
// for non-admins and account_selection "groupmembers" we have to filter by memberOf attribute
|
|
if ($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] === 'groupmembers' &&
|
|
isset($GLOBALS['egw_info']['user']['account_id']) && // only use groupmembers, if we have a user context (stalls login otherwise!)
|
|
(!isset($GLOBALS['egw_info']['user']['apps']['admin'])))
|
|
{
|
|
// generate groupmembers filter once per session without any external calls (but groups) to NOT generate a deep recursion!
|
|
$type_filter .= Api\Cache::getSession(__CLASS__, 'groupmembers_filter', function()
|
|
{
|
|
$account_id = $GLOBALS['egw_info']['user']['account_id'];
|
|
$account = $this->filter(['objectSid' => $this->get_sid($account_id)],
|
|
false, ['memberOf','primaryGroupID','objectSid','samAccountType'])[$account_id];
|
|
$filter = '(|';
|
|
$primary_ignored = in_array(-$account['primarygroupid'][0], $this->ignore_membership);
|
|
foreach($account['memberof'] as $key => $dn)
|
|
{
|
|
if ($key === 'count') continue;
|
|
// if the primary group of the user is ignored (=Domain Users), we assume it's the case for everyone
|
|
if ($primary_ignored)
|
|
{
|
|
$filter .= '(memberOf='.$dn.')';
|
|
continue;
|
|
}
|
|
if (!isset($groups))
|
|
{
|
|
$groups = $this->frontend->search(['type' => 'groups']);
|
|
}
|
|
foreach($groups as $gid => $group)
|
|
{
|
|
if (($gid == -$account['primarygroupid'][0] || $dn === $group['account_dn']) &&
|
|
!in_array($gid, $this->ignore_membership))
|
|
{
|
|
$filter .= '(memberOf='.$group['account_dn'].')(primaryGroupID='.(-$gid).')';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return $filter.')';
|
|
});
|
|
}
|
|
$type_filter .= ')';
|
|
if ($account_type === 'u') break;
|
|
$user_filter = $type_filter;
|
|
// fall through
|
|
case 'g':
|
|
$type_filter = '(|(samaccounttype=' . adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP .
|
|
')(samaccounttype=' . adLDAP::ADLDAP_SECURITY_LOCAL_GROUP . '))';
|
|
if (!empty($this->frontend->config['ads_group_filter']))
|
|
{
|
|
$type_filter = '(&' . $type_filter . $this->frontend->config['ads_group_filter'] . ')';
|
|
}
|
|
if ($account_type === 'g') break;
|
|
// user or groups
|
|
$type_filter = '(|' . $user_filter . $type_filter . ')';
|
|
break;
|
|
}
|
|
return $type_filter;
|
|
}
|
|
|
|
/**
|
|
* Get value(s) for LDAP_CONTROL_SORTREQUEST
|
|
*
|
|
* Sorting by multiple criteria is supported in LDAP RFC 2891, but - at least with Univention Samba - gives wired results,
|
|
* Windows AD does NOT support it and gives an error if the oid is specified!
|
|
*
|
|
* @param ?string $order_by sql order string eg. "contact_email ASC"
|
|
* @return array of arrays with values for keys 'attr', 'oid' (caseIgnoreMatch='2.5.13.3') and 'reverse'
|
|
*/
|
|
protected function sort_values($order_by)
|
|
{
|
|
$values = [];
|
|
while (!empty($order_by) && preg_match("/^(account_)?([^ ]+)( ASC| DESC)?,?/i", $order_by, $matches))
|
|
{
|
|
if (($attr = array_search('account_'.$matches[2], $this->timestamps2egw+$this->other2egw)))
|
|
{
|
|
$values[] = [
|
|
'attr' => $attr,
|
|
// use default match 'oid' => '',
|
|
'reverse' => strtoupper($matches[3]) === ' DESC',
|
|
];
|
|
}
|
|
elseif (($attr = array_search('account_'.$matches[2], $this->attributes2egw)))
|
|
{
|
|
$value = [
|
|
'attr' => $attr,
|
|
'oid' => '2.5.13.3', // caseIgnoreMatch
|
|
'reverse' => strtoupper($matches[3]) === ' DESC',
|
|
];
|
|
// Windows AD does NOT support caseIgnoreMatch sorting, only it's default sorting
|
|
if ($this->serverinfo->activeDirectory(true)) unset($value['oid']);
|
|
$values[] = $value;
|
|
}
|
|
$order_by = substr($order_by, strlen($matches[0]));
|
|
if ($values) break; // sorting by multiple criteria gives no result for Windows AD and wired result for Samba4
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Query ADS by (optional) filter and (optional) account-type filter
|
|
*
|
|
* All reading ADS queries are done throught this methods.
|
|
*
|
|
* @param string|array $attr_filter array with attribute => value pairs or filter string or empty
|
|
* @param string|false $account_type u = user, g = group, default null = try both, false: no type_filter!
|
|
* @param array $attrs =null default return account_lid, else return raw values from ldap-query
|
|
* @param array $accounts =array() array to add filtered accounts too, default empty array
|
|
* @param bool $filter_expired =false true: filter out expired users
|
|
* @param string $order_by sql order string eg. "contact_email ASC"
|
|
* @param ?int $start on return null, if result sorted and limited by server
|
|
* @param int $num_rows number of rows to return if isset($start)
|
|
* @param ?int $total on return total number of rows
|
|
* @return array account_id => account_lid or values for $attrs pairs
|
|
*/
|
|
protected function filter($attr_filter, $account_type=null, array $attrs=null, array $accounts=array(), $filter_expired=false, $order_by=null, &$start=null, $num_rows=null, &$total=null)
|
|
{
|
|
// check if we require sorting and server supports it
|
|
$control = [];
|
|
if (PHP_VERSION >= 7.3 && !empty($order_by) && is_numeric($start) &&
|
|
$this->serverinfo->supportedControl(LDAP_CONTROL_SORTREQUEST, LDAP_CONTROL_VLVREQUEST) &&
|
|
($sort_values = $this->sort_values($order_by)))
|
|
{
|
|
$control = [
|
|
[
|
|
'oid' => LDAP_CONTROL_SORTREQUEST,
|
|
//'iscritical' => TRUE,
|
|
'value' => $sort_values,
|
|
],
|
|
[
|
|
'oid' => LDAP_CONTROL_VLVREQUEST,
|
|
//'iscritical' => TRUE,
|
|
'value' => [
|
|
'before' => 0, // Return 0 entry before target
|
|
'after' => $num_rows-1, // total-1
|
|
'offset' => $start+1, // first = 1, NOT 0!
|
|
'count' => 0, // We have no idea how many entries there are
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
if (!$attr_filter)
|
|
{
|
|
$filter = $this->type_filter($account_type, $filter_expired);
|
|
}
|
|
else
|
|
{
|
|
$filter = '(&';
|
|
if (is_string($attr_filter))
|
|
{
|
|
$filter .= $attr_filter;
|
|
}
|
|
else
|
|
{
|
|
foreach($attr_filter as $attr => $value)
|
|
{
|
|
$filter .= '('.$attr.'='.$this->adldap->utilities()->ldapSlashes($value).')';
|
|
}
|
|
}
|
|
if ($account_type !== false)
|
|
{
|
|
$filter .= $this->type_filter($account_type);
|
|
}
|
|
$filter .= ')';
|
|
}
|
|
if (!($sri = ldap_search($ds=$this->ldap_connection(), $context=$this->ads_context(), $filter,
|
|
$attrs ? $attrs : self::$default_attributes, null, null, null, null, $control)))
|
|
{
|
|
if (($list_view_error = ldap_errno() === 76)) // 76: Virtual List View error --> retry without
|
|
{
|
|
$control = [];
|
|
}
|
|
error_log(__METHOD__.'('.json_encode($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter') returned ".array2string($sri).' '.ldap_error($ds).
|
|
($list_view_error ? ' retrying without virtual list view ...' : ' trying to reconnect ...'));
|
|
|
|
$sri = ldap_search($ds=$this->ldap_connection(!$list_view_error), $context=$this->ads_context(), $filter,
|
|
$attrs ? $attrs : self::$default_attributes, null, null, null, null, $control);
|
|
}
|
|
|
|
if ($sri && ($allValues = ldap_get_entries($ds, $sri)))
|
|
{
|
|
// check if given controls succeeded
|
|
if ($control && ldap_parse_result($ds, $sri, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) &&
|
|
(isset($serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count'])))
|
|
{
|
|
$total = $serverctrls[LDAP_CONTROL_VLVRESPONSE]['value']['count'];
|
|
$start = null; // so caller does NOT run it's own limit
|
|
}
|
|
|
|
foreach($allValues as $key => $data)
|
|
{
|
|
if ($key === 'count') continue;
|
|
|
|
$sid = $data['objectsid'] = $this->adldap->utilities()->getTextSID($data['objectsid'][0]);
|
|
$rid = self::sid2account_id($sid);
|
|
|
|
$accounts[($data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT ? '' : '-').$rid] =
|
|
$attrs ? $data : Api\Translation::convert($data['samaccountname'][0], 'utf-8');
|
|
}
|
|
}
|
|
else if (self::$debug) error_log(__METHOD__.'('.array2string($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter')=$sri allValues=".array2string($allValues));
|
|
|
|
//error_log(date('Y-m-d H:i:s ').__METHOD__.'('.array2string($attr_filter).", '$account_type', ".json_encode($attrs).", ..., expired=$filter_expired, order_by=$order_by, start=$start, num_rows=$num_rows) ldap_search($ds, '$context', '$filter')\n==> returning ".count($accounts)."/$total ".substr(array2string($accounts), 0, 1024)."\n--> ".function_backtrace()."\n\n", 3, '/var/lib/egroupware/ads.log');
|
|
return $accounts;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
public function name2id($name, $which='account_lid', $account_type=null)
|
|
{
|
|
static $to_ldap = array(
|
|
'account_lid' => 'samaccountname',
|
|
'account_email' => 'mail',
|
|
'account_fullname' => 'cn',
|
|
'account_sid' => 'objectsid',
|
|
'account_guid' => 'objectguid',
|
|
);
|
|
$ret = false;
|
|
if (isset($to_ldap[$which]))
|
|
{
|
|
foreach($this->filter(array($to_ldap[$which] => $name), $account_type) as $account_id => $account_lid)
|
|
{
|
|
unset($account_lid);
|
|
$ret = $account_id;
|
|
break;
|
|
}
|
|
}
|
|
if (self::$debug) error_log(__METHOD__."('$name', '$which', '$account_type') returning ".array2string($ret));
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Convert an numeric account_id to any other value of that account (account_lid, account_email, ...)
|
|
*
|
|
* Calls frontend which uses (cached) read method to fetch all data by account_id.
|
|
*
|
|
* @param int $account_id numeric 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)
|
|
*/
|
|
public function id2name($account_id, $which='account_lid')
|
|
{
|
|
return $this->frontend->id2name($account_id,$which);
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
unset($_account_id, $ip); // not used, but required by function signature
|
|
|
|
return false; // not longer supported
|
|
}
|
|
|
|
/**
|
|
* Query memberships of a given account
|
|
*
|
|
* Calls frontend which uses (cached) read method to fetch all data by account_id.
|
|
*
|
|
* @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 (!($data = $this->frontend->read($account_id)) || $data['account_id'] <= 0) return false;
|
|
|
|
return $data['memberships'];
|
|
}
|
|
|
|
/**
|
|
* Query the members of a group
|
|
*
|
|
* Calls frontend which uses (cached) read method to fetch all data by account_id.
|
|
*
|
|
* @param int $gid
|
|
* @return array with uidnumber => uid pairs
|
|
*/
|
|
function members($gid)
|
|
{
|
|
if (!($data = $this->frontend->read($gid)) || $data['account_id'] >= 0) return false;
|
|
|
|
return $data['members'];
|
|
}
|
|
|
|
/**
|
|
* Sets the memberships of the given account
|
|
*
|
|
* @param array $groups array with gidnumbers
|
|
* @param int $account_id uidnumber
|
|
* @return int number of added or removed memberships
|
|
*/
|
|
function set_memberships($groups,$account_id)
|
|
{
|
|
if (!($account = $this->id2name($account_id))) return;
|
|
$current = array_keys($this->memberships($account_id));
|
|
|
|
$changed = 0;
|
|
foreach(array(
|
|
'add' => array_diff($groups, $current), // add account to all groups he is currently not in
|
|
'remove' => array_diff($current, $groups), // remove account from all groups he is only currently in
|
|
) as $op => $memberships)
|
|
{
|
|
$func = $op.($account_id > 0 ? 'User' : 'Group');
|
|
foreach($memberships as $gid)
|
|
{
|
|
$ok = $this->adldap->group()->$func($group=$this->id2name($gid), $account);
|
|
//error_log(__METHOD__.'('.array2string($groups).", $account_id) $func('$group', '$account') returned ".array2string($ok));
|
|
$changed += (int)$ok;
|
|
}
|
|
}
|
|
if (self::$debug) error_log(__METHOD__.'('.array2string($groups).", $account_id) current=".array2string($current)." returning $changed");
|
|
return $changed;
|
|
}
|
|
|
|
/**
|
|
* Set the members of a group
|
|
*
|
|
* @param array $users array with uidnumber or uid's
|
|
* @param int $gid gidnumber of group to set
|
|
* @return int number of added or removed members
|
|
*/
|
|
function set_members($users, $gid)
|
|
{
|
|
if (!($group = $this->id2name($gid))) return;
|
|
$current = array_keys($this->members($gid));
|
|
|
|
$changed = 0;
|
|
foreach(array(
|
|
'add' => array_diff($users, $current), // add members currently not in
|
|
'remove' => array_diff($current, $users), // remove members only currently in
|
|
) as $op => $members)
|
|
{
|
|
foreach($members as $account_id)
|
|
{
|
|
$func = $op.($account_id > 0 ? 'User' : 'Group');
|
|
$ok = $this->adldap->group()->$func($group, $account=$this->id2name($account_id));
|
|
//error_log(__METHOD__.'('.array2string($users).", $account_id) $func('$group', '$account') returned ".array2string($ok));
|
|
$changed += (int)$ok;
|
|
}
|
|
}
|
|
if (self::$debug) error_log(__METHOD__.'('.array2string($users).", $gid) current=".array2string($current)." returning $changed");
|
|
return $changed;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixes and enhancements for adLDAP required by EGroupware
|
|
*
|
|
* - allow to use utf-8 charset internally, not just an 8-bit iso-charset
|
|
* - support for Windows2008r2 (maybe earlier too) and Samba4 "CN=Users" DN as container to create users or groups
|
|
*/
|
|
class adLDAP extends \adLDAP
|
|
{
|
|
/**
|
|
* Charset used for internal encoding
|
|
*
|
|
* @var string
|
|
*/
|
|
public $charset = 'iso-8859-1';
|
|
|
|
function __construct(array $options=array())
|
|
{
|
|
if (isset($options['charset']))
|
|
{
|
|
$this->charset = strtolower($options['charset']);
|
|
}
|
|
parent::__construct($options);
|
|
}
|
|
|
|
/**
|
|
* Convert 8bit characters e.g. accented characters to UTF8 encoded characters
|
|
*
|
|
* Extended to use mbstring to convert from arbitrary charset to utf-8
|
|
*/
|
|
public function encode8Bit(&$item, $key)
|
|
{
|
|
if ($this->charset != 'utf-8' && $key != 'password')
|
|
{
|
|
if (function_exists('mb_convert_encoding'))
|
|
{
|
|
$item = mb_convert_encoding($item, 'utf-8', $this->charset);
|
|
}
|
|
else
|
|
{
|
|
parent::encode8Bit($item, $key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the userclass interface
|
|
*
|
|
* @return adLDAPUsers
|
|
*/
|
|
public function user() {
|
|
if (!$this->userClass) {
|
|
$this->userClass = new adLDAPUsers($this);
|
|
}
|
|
return $this->userClass;
|
|
}
|
|
|
|
/**
|
|
* Get the group class interface
|
|
*
|
|
* @return adLDAPGroups
|
|
*/
|
|
public function group() {
|
|
if (!$this->groupClass) {
|
|
$this->groupClass = new adLDAPGroups($this);
|
|
}
|
|
return $this->groupClass;
|
|
}
|
|
|
|
/**
|
|
* Get the utils class interface
|
|
*
|
|
* @return adLDAPUtils
|
|
*/
|
|
public function utilities() {
|
|
if (!$this->utilClass) {
|
|
$this->utilClass = new adLDAPUtils($this);
|
|
}
|
|
return $this->utilClass;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixes an enhancements for adLDAPUser required by EGroupware
|
|
*/
|
|
class adLDAPUsers extends \adLDAPUsers
|
|
{
|
|
/**
|
|
* Create a user
|
|
*
|
|
* Extended to allow to specify $attribute["container"] as string, because array hardcodes "OU=", while Samba4 and win2008r2 uses "CN=Users"
|
|
*
|
|
* Extended to ensure following creating order required by at least win2008r2:
|
|
* - new user without password and deactivated
|
|
* - add password, see new method setPassword
|
|
* - activate user
|
|
*
|
|
* @param array $attributes The attributes to set to the user account
|
|
* @return bool
|
|
*/
|
|
public function create($attributes)
|
|
{
|
|
// Check for compulsory fields
|
|
if (!array_key_exists("username", $attributes)){ return "Missing compulsory field [username]"; }
|
|
if (!array_key_exists("firstname", $attributes)){ return "Missing compulsory field [firstname]"; }
|
|
if (!array_key_exists("surname", $attributes)){ return "Missing compulsory field [surname]"; }
|
|
if (!array_key_exists("email", $attributes)){ return "Missing compulsory field [email]"; }
|
|
if (!array_key_exists("container", $attributes)){ return "Missing compulsory field [container]"; }
|
|
if (empty($attributes["container"])){ return "Container attribute must be an array or string."; }
|
|
|
|
if (array_key_exists("password",$attributes) && (!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS())){
|
|
throw new adLDAPException('SSL must be configured on your webserver and enabled in the class to set passwords.');
|
|
}
|
|
|
|
if (!array_key_exists("display_name", $attributes)) {
|
|
$attributes["display_name"] = $attributes["firstname"] . " " . $attributes["surname"];
|
|
}
|
|
|
|
// Translate the schema
|
|
$add = $this->adldap->adldap_schema($attributes);
|
|
|
|
// Additional stuff only used for adding accounts
|
|
$add["cn"][0] = $attributes["username"];
|
|
$add["samaccountname"][0] = $attributes["username"];
|
|
$add["userPrincipalName"][0] = $attributes["username"].$this->adldap->getAccountSuffix();
|
|
$add["objectclass"][0] = "top";
|
|
$add["objectclass"][1] = "person";
|
|
$add["objectclass"][2] = "organizationalPerson";
|
|
$add["objectclass"][3] = "user"; //person?
|
|
//$add["name"][0]=$attributes["firstname"]." ".$attributes["surname"];
|
|
|
|
// Set the account control attribute
|
|
$control_options = array("NORMAL_ACCOUNT", "ACCOUNTDISABLE");
|
|
$add["userAccountControl"][0] = $this->accountControl($control_options);
|
|
|
|
// Determine the container
|
|
if (is_array($attributes['container'])) {
|
|
$attributes["container"] = array_reverse($attributes["container"]);
|
|
$attributes["container"] = "OU=" . implode(",OU=",$attributes["container"]);
|
|
}
|
|
// we can NOT set password with ldap_add or ldap_modify, it needs ldap_mod_replace, at least under Win2008r2
|
|
unset($add['unicodePwd']);
|
|
|
|
// Add the entry
|
|
$result = ldap_add($ds=$this->adldap->getLdapConnection(), $dn="CN=" . $add["cn"][0] . "," . $attributes["container"] . "," . $this->adldap->getBaseDn(), $add);
|
|
if ($result != true) {
|
|
error_log(__METHOD__."(".array2string($attributes).") ldap_add($ds, '$dn', ".array2string($add).") returned ".array2string($result)." ldap_error()=".ldap_error($ds));
|
|
return false;
|
|
}
|
|
|
|
// now password can be added to still disabled account
|
|
if (array_key_exists("password",$attributes))
|
|
{
|
|
if (!$this->setPassword($dn, $attributes['password'])) return false;
|
|
|
|
// now account can be enabled
|
|
if ($attributes["enabled"])
|
|
{
|
|
$control_options = array("NORMAL_ACCOUNT");
|
|
$mod = array("userAccountControl" => $this->accountControl($control_options));
|
|
$result = ldap_modify($ds, $dn, $mod);
|
|
if (!$result) error_log(__METHOD__."(".array2string($attributes).") ldap_modify($ds, '$dn', ".array2string($mod).") returned ".array2string($result)." ldap_error()=".ldap_error($ds));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Encode a password for transmission over LDAP
|
|
*
|
|
* Extended to use mbstring to convert from arbitrary charset to UTF-16LE
|
|
*
|
|
* @param string $password The password to encode
|
|
* @return string
|
|
*/
|
|
public function encodePassword($password)
|
|
{
|
|
$password="\"".$password."\"";
|
|
if (function_exists('mb_convert_encoding') && !empty($this->adldap->charset))
|
|
{
|
|
return mb_convert_encoding($password, 'UTF-16LE', $this->adldap->charset);
|
|
}
|
|
$encoded="";
|
|
for ($i=0, $len=strlen($password); $i < $len; $i++)
|
|
{
|
|
$encoded .= $password[$i]."\000";
|
|
}
|
|
return $encoded;
|
|
}
|
|
|
|
/**
|
|
* Set a password
|
|
*
|
|
* Requires "Reset password" priviledges from bind user!
|
|
*
|
|
* We can NOT set password with ldap_add or ldap_modify, it needs ldap_mod_replace, at least under Win2008r2!
|
|
*
|
|
* @param string $dn
|
|
* @param string $password
|
|
* @return boolean
|
|
*/
|
|
public function setPassword($dn, $password)
|
|
{
|
|
$result = ldap_mod_replace($ds=$this->adldap->getLdapConnection(), $dn, array(
|
|
'unicodePwd' => $this->encodePassword($password),
|
|
));
|
|
if (!$result) error_log(__METHOD__."('$dn', '$password') ldap_mod_replace($ds, '$dn', \$password) returned FALSE: ".ldap_error($ds));
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check if we can to a real password change, not just a password reset
|
|
*
|
|
* Requires PHP 5.4 >= 5.4.26, PHP 5.5 >= 5.5.10 or PHP 5.6 >= 5.6.0
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public static function changePasswordSupported()
|
|
{
|
|
return function_exists('ldap_modify_batch');
|
|
}
|
|
|
|
/**
|
|
* Set the password of a user - This must be performed over SSL
|
|
*
|
|
* @param string $username The username to modify
|
|
* @param string $password The new password
|
|
* @param bool $isGUID Is the username passed a GUID or a samAccountName
|
|
* @param string $old_password old password for password change, if supported
|
|
* @return bool
|
|
*/
|
|
public function password($username, $password, $isGUID = false, $old_password=null)
|
|
{
|
|
if ($username === NULL) { return false; }
|
|
if ($password === NULL) { return false; }
|
|
if (!$this->adldap->getLdapBind()) { return false; }
|
|
if (!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()) {
|
|
throw new adLDAPException('SSL must be configured on your webserver and enabled in the class to set passwords.');
|
|
}
|
|
|
|
$userDn = $this->dn($username, $isGUID);
|
|
if ($userDn === false) {
|
|
return false;
|
|
}
|
|
|
|
$add=array();
|
|
|
|
if (empty($old_password) || !function_exists('ldap_modify_batch')) {
|
|
$add["unicodePwd"][0] = $this->encodePassword($password);
|
|
|
|
$result = @ldap_mod_replace($this->adldap->getLdapConnection(), $userDn, $add);
|
|
}
|
|
else {
|
|
$mods = array(
|
|
array(
|
|
"attrib" => "unicodePwd",
|
|
"modtype" => LDAP_MODIFY_BATCH_REMOVE,
|
|
"values" => array($this->encodePassword($old_password)),
|
|
),
|
|
array(
|
|
"attrib" => "unicodePwd",
|
|
"modtype" => LDAP_MODIFY_BATCH_ADD,
|
|
"values" => array($this->encodePassword($password)),
|
|
),
|
|
);
|
|
$result = ldap_modify_batch($this->adldap->getLdapConnection(), $userDn, $mods);
|
|
}
|
|
if ($result === false){
|
|
$err = ldap_errno($this->adldap->getLdapConnection());
|
|
if ($err) {
|
|
$msg = 'Error ' . $err . ': ' . ldap_err2str($err) . '.';
|
|
if($err == 53) {
|
|
$msg .= ' Your password might not match the password policy.';
|
|
}
|
|
throw new adLDAPException($msg);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Modify a user
|
|
*
|
|
* @param string $username The username to query
|
|
* @param array $attributes The attributes to modify. Note if you set the enabled attribute you must not specify any other attributes
|
|
* @param bool $isGUID Is the username passed a GUID or a samAccountName
|
|
* @return bool
|
|
*/
|
|
public function modify($username, $attributes, $isGUID = false)
|
|
{
|
|
if ($username === NULL) { return "Missing compulsory field [username]"; }
|
|
if (array_key_exists("password", $attributes) && !$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()) {
|
|
throw new adLDAPException('SSL/TLS must be configured on your webserver and enabled in the class to set passwords.');
|
|
}
|
|
|
|
// Find the dn of the user
|
|
$userDn = $this->dn($username, $isGUID);
|
|
if ($userDn === false) {
|
|
return false;
|
|
}
|
|
|
|
// Translate the update to the LDAP schema
|
|
$mod = $this->adldap->adldap_schema($attributes);
|
|
|
|
// Check to see if this is an enabled status update
|
|
if (!$mod && !array_key_exists("enabled", $attributes)){
|
|
return false;
|
|
}
|
|
|
|
// Set the account control attribute (only if specified)
|
|
if (array_key_exists("enabled", $attributes)){
|
|
if ($attributes["enabled"]){
|
|
$controlOptions = array("NORMAL_ACCOUNT");
|
|
}
|
|
else {
|
|
$controlOptions = array("NORMAL_ACCOUNT", "ACCOUNTDISABLE");
|
|
}
|
|
$mod["userAccountControl"][0] = $this->accountControl($controlOptions);
|
|
}
|
|
// we can NOT set password with ldap_add or ldap_modify, it needs ldap_mod_replace, at least under Win2008r2
|
|
unset($mod['unicodePwd']);
|
|
|
|
if ($mod)
|
|
{
|
|
// Do the update
|
|
$result = @ldap_modify($ds=$this->adldap->getLdapConnection(), $userDn, $mod);
|
|
if ($result == false) {
|
|
if (isset($mod['unicodePwd'])) $mod['unicodePwd'] = '***';
|
|
error_log(__METHOD__."(".array2string($attributes).") ldap_modify($ds, '$userDn', ".array2string($mod).") returned ".array2string($result)." ldap_error()=".ldap_error($ds));
|
|
return false;
|
|
}
|
|
}
|
|
if (array_key_exists("password",$attributes) && !$this->setPassword($userDn, $attributes['password']))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixes an enhancements for adLDAPGroups required by EGroupware
|
|
*/
|
|
class adLDAPGroups extends \adLDAPGroups
|
|
{
|
|
/**
|
|
* Create a group
|
|
*
|
|
* Extended to allow to specify $attribute["container"] as string, because array hardcodes "OU=", while Samba4 and win2008r2 uses "CN=Users"
|
|
*
|
|
* @param array $attributes Default attributes of the group
|
|
* @return bool
|
|
*/
|
|
public function create($attributes)
|
|
{
|
|
if (!is_array($attributes)){ return "Attributes must be an array"; }
|
|
if (!array_key_exists("group_name", $attributes)){ return "Missing compulsory field [group_name]"; }
|
|
if (!array_key_exists("container", $attributes)){ return "Missing compulsory field [container]"; }
|
|
if (empty($attributes["container"])){ return "Container attribute must be an array or string."; }
|
|
|
|
//$member_array = array();
|
|
//$member_array[0] = "cn=user1,cn=Users,dc=yourdomain,dc=com";
|
|
//$member_array[1] = "cn=administrator,cn=Users,dc=yourdomain,dc=com";
|
|
|
|
$add = array();
|
|
$add["cn"] = $attributes["group_name"];
|
|
$add["samaccountname"] = $attributes["group_name"];
|
|
$add["objectClass"] = "Group";
|
|
if (!empty($attributes["description"])) $add["description"] = $attributes["description"];
|
|
//$add["member"] = $member_array; UNTESTED
|
|
|
|
// Determine the container
|
|
if (is_array($attributes['container'])) {
|
|
$attributes["container"] = array_reverse($attributes["container"]);
|
|
$attributes["container"] = "OU=" . implode(",OU=",$attributes["container"]);
|
|
}
|
|
$result = ldap_add($this->adldap->getLdapConnection(), "CN=" . $add["cn"] . "," . $attributes["container"] . "," . $this->adldap->getBaseDn(), $add);
|
|
if ($result != true) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixes an enhancements for adLDAPUtils required by EGroupware
|
|
*/
|
|
class adLDAPUtils extends \adLDAPUtils
|
|
{
|
|
/**
|
|
* Convert 8bit characters e.g. accented characters to UTF8 encoded characters
|
|
*/
|
|
public function encode8Bit(&$item, $key)
|
|
{
|
|
$encode = false;
|
|
if (is_string($item)) {
|
|
for ($i = 0, $len=strlen($item); $i < $len; $i++) {
|
|
if (ord($item[$i]) >> 7) {
|
|
$encode = true;
|
|
}
|
|
}
|
|
}
|
|
if ($encode === true && $key != 'password') {
|
|
$item = utf8_encode($item);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape strings for the use in LDAP filters
|
|
*
|
|
* DEVELOPERS SHOULD BE DOING PROPER FILTERING IF THEY'RE ACCEPTING USER INPUT
|
|
* Ported from Perl's Net::LDAP::Util escape_filter_value
|
|
*
|
|
* @param string $str The string the parse
|
|
* @author Port by Andreas Gohr <andi@splitbrain.org>
|
|
* @return string
|
|
*/
|
|
public function ldapSlashes($str){
|
|
return preg_replace_callback(
|
|
'/([\x00-\x1F\*\(\)\\\\])/',
|
|
function ($matches) {
|
|
return "\\".join("", unpack("H2", $matches[1]));
|
|
},
|
|
$str
|
|
);
|
|
}
|
|
}
|