7 * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
8 * @copyright Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
9 * @author Lars Kneschke <l.kneschke@metaways.de>
13 * User Samba4 ldap backend
18 class Tinebase_User_ActiveDirectory extends Tinebase_User_Ldap
20 const ACCOUNTDISABLE = 2;
21 const NORMAL_ACCOUNT = 512;
24 * mapping of ldap attributes to class properties
28 protected $_rowNameMapping = array(
29 'accountDisplayName' => 'displayname',
30 'accountFullName' => 'cn',
31 'accountFirstName' => 'givenname',
32 'accountLastName' => 'sn',
33 'accountLoginName' => 'samaccountname',
34 'accountLastPasswordChange' => 'pwdlastset',
35 'accountExpires' => 'accountexpires',
36 'accountPrimaryGroup' => 'primarygroupid',
37 'accountEmailAddress' => 'mail',
39 'profilePath' => 'profilepath',
40 'logonScript' => 'scriptpath',
41 'homeDrive' => 'homedrive',
42 'homePath' => 'homedirectory',
44 #'accountStatus' => 'shadowinactive'
48 * objectclasses required by this backend
52 protected $_requiredObjectClass = array(
56 'organizationalPerson'
60 * the basic group ldap filter (for example the objectclass)
64 protected $_groupBaseFilter = 'objectclass=group';
67 * the basic user ldap filter (for example the objectclass)
71 protected $_userBaseFilter = 'objectclass=user';
73 protected $_isReadOnlyBackend = false;
78 * @param array $_options Options used in connecting, binding, etc.
79 * @throws Tinebase_Exception_Backend_Ldap
81 public function __construct(array $_options = array())
83 if(empty($_options['userUUIDAttribute'])) {
84 $_options['userUUIDAttribute'] = 'objectGUID';
86 if(empty($_options['groupUUIDAttribute'])) {
87 $_options['groupUUIDAttribute'] = 'objectGUID';
89 if(empty($_options['baseDn'])) {
90 $_options['baseDn'] = $_options['userDn'];
92 if(empty($_options['userFilter'])) {
93 $_options['userFilter'] = 'objectclass=user';
95 if(empty($_options['userSearchScope'])) {
96 $_options['userSearchScope'] = Zend_Ldap::SEARCH_SCOPE_SUB;
98 if(empty($_options['groupFilter'])) {
99 $_options['groupFilter'] = 'objectclass=group';
102 parent::__construct($_options);
104 if ($this->_options['useRfc2307']) {
105 $this->_requiredObjectClass[] = 'posixAccount';
106 $this->_requiredObjectClass[] = 'shadowAccount';
108 $this->_rowNameMapping['accountHomeDirectory'] = 'unixhomedirectory';
109 $this->_rowNameMapping['accountLoginShell'] = 'loginshell';
113 $this->_domainConfig = $this->_ldap->search(
114 'objectClass=domain',
115 $this->_ldap->getFirstNamingContext(),
116 Zend_Ldap::SEARCH_SCOPE_BASE
119 $this->_domainSidBinary = $this->_domainConfig['objectsid'][0];
120 $this->_domainSidPlain = Tinebase_Ldap::decodeSid($this->_domainConfig['objectsid'][0]);
122 $domanNameParts = array();
123 $keys = null; // not really needed
124 Zend_Ldap_Dn::explodeDn($this->_domainConfig['distinguishedname'][0], $keys, $domanNameParts);
125 $this->_domainName = implode('.', $domanNameParts);
131 * @param Tinebase_Model_FullUser $_user
132 * @return Tinebase_Model_FullUser|NULL
134 public function addUserToSyncBackend(Tinebase_Model_FullUser $_user)
136 if ($this->_isReadOnlyBackend) {
140 $ldapData = $this->_user2ldap($_user);
142 // will be added later
143 $primaryGroupId = $ldapData['primarygroupid'];
144 unset($ldapData['primarygroupid']);
146 $ldapData['objectclass'] = $this->_requiredObjectClass;
148 foreach ($this->_ldapPlugins as $plugin) {
149 $plugin->inspectAddUser($_user, $ldapData);
152 $dn = $this->_generateDn($_user);
154 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
155 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ldapData: ' . print_r($ldapData, true));
157 $this->_ldap->add($dn, $ldapData);
159 $userId = $this->_ldap->getEntry($dn, array($this->_userUUIDAttribute));
160 $userId = $this->_decodeAccountId($userId[$this->_userUUIDAttribute][0]);
162 // add user to primary group and set primary group
163 Tinebase_Group::getInstance()->addGroupMemberInSyncBackend($_user->accountPrimaryGroup, $userId);
165 // set primary goup id
166 $this->_ldap->updateProperty($dn, array('primarygroupid' => $primaryGroupId));
169 $user = $this->getUserByPropertyFromSyncBackend('accountId', $userId, 'Tinebase_Model_FullUser');
175 * sets/unsets expiry date in ldap backend
177 * @param mixed $_accountId
178 * @param Tinebase_DateTime $_expiryDate
180 public function setExpiryDateInSyncBackend($_accountId, $_expiryDate)
182 if ($this->_isReadOnlyBackend) {
186 $metaData = $this->_getMetaData($_accountId);
188 if ($_expiryDate instanceof DateTime) {
189 $ldapData['accountexpires'] = bcmul(bcadd($_expiryDate->getTimestamp(), '11644473600'), '10000000');
191 if ($this->_options['useRfc2307']) {
192 // days since Jan 1, 1970
193 $ldapData = array_merge($ldapData, array(
194 'shadowexpire' => floor($_expiryDate->getTimestamp() / 86400)
199 'accountexpires' => '9223372036854775807'
202 if ($this->_options['useRfc2307']) {
203 $ldapData = array_merge($ldapData, array(
204 'shadowexpire' => array()
209 foreach ($this->_ldapPlugins as $plugin) {
210 $plugin->inspectExpiryDate($_expiryDate, $ldapData);
213 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']} $ldapData: " . print_r($ldapData, true));
215 $this->_ldap->update($metaData['dn'], $ldapData);
219 * set the password for given account
221 * @param string $_userId
222 * @param string $_password
223 * @param bool $_encrypt encrypt password
224 * @param bool $_mustChange
226 * @throws Tinebase_Exception_InvalidArgument
228 public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
230 if ($this->_isReadOnlyBackend) {
234 $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($_userId);
236 $this->checkPasswordPolicy($_password, $user);
238 $metaData = $this->_getMetaData($user);
241 'unicodePwd' => $this->_encodePassword($_password),
244 if ($this->_options['useRfc2307']) {
245 $ldapData = array_merge($ldapData, array(
246 'shadowlastchange' => floor(Tinebase_DateTime::now()->getTimestamp() / 86400)
250 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
251 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $dn: ' . $metaData['dn']);
252 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
253 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' $ldapData: ' . print_r($ldapData, true));
255 $this->_ldap->updateProperty($metaData['dn'], $ldapData);
257 // update last modify timestamp in sql backend too
259 'last_password_change' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
263 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
266 $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
268 $this->_setPluginsPassword($user->getId(), $_password, $_encrypt);
272 * update user status (enabled or disabled)
274 * @param mixed $_accountId
275 * @param string $_status
277 public function setStatusInSyncBackend($_accountId, $_status)
279 if ($this->_isReadOnlyBackend) {
283 $metaData = $this->_getMetaData($_accountId);
285 if ($_status == 'enabled') {
287 'useraccountcontrol' => $metaData['useraccountcontrol'][0] &= ~self::ACCOUNTDISABLE
289 if ($this->_options['useRfc2307']) {
290 $ldapData = array_merge($ldapData, array(
291 'shadowMax' => 999999,
292 'shadowInactive' => array()
297 'useraccountcontrol' => $metaData['useraccountcontrol'][0] |= self::ACCOUNTDISABLE
299 if ($this->_options['useRfc2307']) {
300 $ldapData = array_merge($ldapData, array(
302 'shadowInactive' => 1
307 foreach ($this->_ldapPlugins as $plugin) {
308 $plugin->inspectStatus($_status, $ldapData);
311 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
312 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']} ldapData: " . print_r($ldapData, true));
314 $this->_ldap->update($metaData['dn'], $ldapData);
318 * updates an existing user
320 * @todo check required objectclasses?
322 * @param Tinebase_Model_FullUser $_account
323 * @return Tinebase_Model_FullUser
325 public function updateUserInSyncBackend(Tinebase_Model_FullUser $_account)
327 if ($this->_isReadOnlyBackend) {
331 Tinebase_Group::getInstance()->addGroupMemberInSyncBackend($_account->accountPrimaryGroup, $_account->getId());
333 $ldapEntry = $this->_getLdapEntry('accountId', $_account);
335 $ldapData = $this->_user2ldap($_account, $ldapEntry);
337 foreach ($this->_ldapPlugins as $plugin) {
338 $plugin->inspectUpdateUser($_account, $ldapData, $ldapEntry);
341 // do we need to rename the entry?
342 // TODO move to rename()
343 $dn = Zend_Ldap_Dn::factory($ldapEntry['dn'], null);
344 $rdn = $dn->getRdn();
345 if ($rdn['CN'] != $ldapData['cn']) {
346 $newDN = $this->_generateDn($_account);
347 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
348 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' rename ldap entry to: ' . $newDN);
349 $this->_ldap->rename($dn, $newDN);
352 // no need to update this attribute, it's not allowed to change and even might not be updateable
353 unset($ldapData[$this->_userUUIDAttribute]);
355 // remove cn as samba forbids updating the CN (even if it does not change...
356 // 0x43 (Operation not allowed on RDN; 00002016: Modify of RDN 'CN' on CN=...,CN=Users,DC=example,DC=org
357 // not permitted, must use 'rename' operation instead
358 unset($ldapData['cn']);
360 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
361 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $dn: ' . $ldapEntry['dn']);
362 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
363 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' $ldapData: ' . print_r($ldapData, true));
365 $this->_ldap->update($ldapEntry['dn'], $ldapData);
367 // refetch user from ldap backend
368 $user = $this->getUserByPropertyFromSyncBackend('accountId', $_account, 'Tinebase_Model_FullUser');
374 * convert binary id to plain text id
376 * @param string $accountId
379 protected function _decodeAccountId($accountId)
381 switch ($this->_userUUIDAttribute) {
383 return Tinebase_Ldap::decodeGuid($accountId);
387 return Tinebase_Ldap::decodeSid($accountId);
397 * convert plain text id to binary id
399 * @param string $accountId
402 protected function _encodeAccountId($accountId)
404 switch ($this->_userUUIDAttribute) {
406 return Tinebase_Ldap::encodeGuid($accountId);
417 * generates dn for new user
419 * @param Tinebase_Model_FullUser $_account
422 protected function _generateDn(Tinebase_Model_FullUser $_account)
424 $newDn = "cn={$_account->accountFullName},{$this->_baseDn}";
430 * Returns a user obj with raw data from ldap
432 * @param array $_userData
433 * @param string $_accountClass
434 * @return Tinebase_Record_Abstract
436 protected function _ldap2User(array $_userData, $_accountClass = 'Tinebase_Model_FullUser')
440 foreach ($_userData as $key => $value) {
444 $keyMapping = array_search($key, $this->_rowNameMapping);
445 if ($keyMapping !== FALSE) {
446 switch($keyMapping) {
447 case 'accountExpires':
448 if ($value === '0' || $value[0] === '9223372036854775807') {
449 $accountArray[$keyMapping] = null;
451 $accountArray[$keyMapping] = self::convertADTimestamp($value[0]);
455 case 'accountLastPasswordChange':
456 $accountArray[$keyMapping] = self::convertADTimestamp($value[0]);
460 $accountArray[$keyMapping] = $this->_decodeAccountId($value[0]);
465 $accountArray[$keyMapping] = $value[0];
471 $accountArray['accountStatus'] = (isset($_userData['useraccountcontrol']) && ($_userData['useraccountcontrol'][0] & self::ACCOUNTDISABLE)) ? 'disabled' : 'enabled';
472 if ($accountArray['accountExpires'] instanceof Tinebase_DateTime && Tinebase_DateTime::now()->compare($accountArray['accountExpires']) == -1) {
473 $accountArray['accountStatus'] = 'disabled';
477 $maxPasswordAge = abs(bcdiv($this->_domainConfig['maxpwdage'][0], '10000000'));
478 if ($maxPasswordAge > 0 && isset($accountArray['accountLastPasswordChange'])) {
479 $accountArray['accountExpires'] = clone $accountArray['accountLastPasswordChange'];
480 $accountArray['accountExpires']->addSecond($maxPasswordAge);
482 if (Tinebase_DateTime::now()->compare($accountArray['accountExpires']) == -1) {
483 $accountArray['accountStatus'] = 'disabled';
487 if (empty($accountArray['accountLastName']) && !empty($accountArray['accountFullName'])) {
488 $accountArray['accountLastName'] = $accountArray['accountFullName'];
492 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not instantiate account object for ldap user ' . print_r($_userData, 1));
493 $accountObject = null;
495 $accountObject = new $_accountClass($accountArray, TRUE);
498 if ($accountObject instanceof Tinebase_Model_FullUser) {
499 $accountObject->sambaSAM = new Tinebase_Model_SAMUser($accountArray);
502 return $accountObject;
506 * convert windows nt timestamp
508 * The timestamp is the number of 100-nanoseconds intervals (1 nanosecond = one billionth of a second)
509 * since Jan 1, 1601 UTC.
512 * @return Tinebase_DateTime
514 * @see http://www.epochconverter.com/epoch/ldap-timestamp.php
516 public static function convertADTimestamp($timestamp)
518 return new Tinebase_DateTime(bcsub(bcdiv($timestamp, '10000000'), '11644473600'));
522 * returns array of ldap data
524 * @param Tinebase_Model_FullUser $_user
527 protected function _user2ldap(Tinebase_Model_FullUser $_user, array $_ldapEntry = array())
530 'useraccountcontrol' => isset($_ldapEntry['useraccountcontrol']) ? $_ldapEntry['useraccountcontrol'][0] : self::NORMAL_ACCOUNT
533 foreach ($_user as $key => $value) {
534 $ldapProperty = (isset($this->_rowNameMapping[$key]) || array_key_exists($key, $this->_rowNameMapping)) ? $this->_rowNameMapping[$key] : false;
536 if ($ldapProperty === false) {
541 case 'accountLastPasswordChange':
545 case 'accountExpires':
546 if ($value instanceof DateTime) {
547 $ldapData[$ldapProperty] = bcmul(bcadd($value->getTimestamp(), '11644473600'), '10000000');
549 $ldapData[$ldapProperty] = '9223372036854775807';
553 case 'accountStatus':
554 if ($value == 'enabled') {
555 // unset account disable flag
556 $ldapData['useraccountcontrol'] &= ~self::ACCOUNTDISABLE;
558 // set account disable flag
559 $ldapData['useraccountcontrol'] |= self::ACCOUNTDISABLE;
563 case 'accountPrimaryGroup':
564 $ldapData[$ldapProperty] = Tinebase_Group::getInstance()->resolveUUIdToGIdNumber($value);
565 if ($this->_options['useRfc2307']) {
566 $ldapData['gidNumber'] = Tinebase_Group::getInstance()->resolveGidNumber($value);
571 $ldapData[$ldapProperty] = $value;
576 $ldapData['name'] = $ldapData['cn'];
577 $ldapData['userPrincipalName'] = $_user->accountLoginName . '@' . $this->_domainName;
579 if ($this->_options['useRfc2307']) {
580 // homedir is an required attribute
581 if (empty($ldapData['unixhomedirectory'])) {
582 $ldapData['unixhomedirectory'] = '/dev/null';
585 // set uidNumber only when not set in AD already
586 if (empty($_ldapEntry['uidnumber'])) {
587 $ldapData['uidnumber'] = $this->_generateUidNumber();
589 $ldapData['gidnumber'] = Tinebase_Group::getInstance()->resolveGidNumber($_user->accountPrimaryGroup);
591 $ldapData['msSFU30NisDomain'] = Tinebase_Helper::array_value(0, explode('.', $this->_domainName));
594 if (isset($_user->sambaSAM) && $_user->sambaSAM instanceof Tinebase_Model_SAMUser) {
595 $ldapData['profilepath'] = $_user->sambaSAM->profilePath;
596 $ldapData['scriptpath'] = $_user->sambaSAM->logonScript;
597 $ldapData['homedrive'] = $_user->sambaSAM->homeDrive;
598 $ldapData['homedirectory'] = $_user->sambaSAM->homePath;
602 $ldapData['objectclass'] = isset($_ldapEntry['objectclass']) ? $_ldapEntry['objectclass'] : array();
604 // check if user has all required object classes. This is needed
605 // when updating users which where created using different requirements
606 foreach ($this->_requiredObjectClass as $className) {
607 if (! in_array($className, $ldapData['objectclass'])) {
608 // merge all required classes at once
609 $ldapData['objectclass'] = array_unique(array_merge($ldapData['objectclass'], $this->_requiredObjectClass));
614 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
615 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' LDAP data ' . print_r($ldapData, true));
621 * Encode a password to UTF-16LE
623 * @param string $password the plain password
627 protected function _encodePassword($password)
629 $password = '"' . $password . '"';
630 $passwordLength = strlen($password);
632 $encodedPassword = null;
634 for ($pos = 0; $pos < $passwordLength; $pos++) {
635 $encodedPassword .= "{$password{$pos}}\000";
638 return $encodedPassword;