2ca44b1a2cf092c3e424ff3a7fbc5f3e9364b3dc
[tine20] / tine20 / Tinebase / User / ActiveDirectory.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  User
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>
10  */
11
12 /**
13  * User Samba4 ldap backend
14  *
15  * @package     Tinebase
16  * @subpackage  User
17  */
18 class Tinebase_User_ActiveDirectory extends Tinebase_User_Ldap
19 {
20     const ACCOUNTDISABLE = 2;
21     const NORMAL_ACCOUNT = 512;
22
23     /**
24      * mapping of ldap attributes to class properties
25      *
26      * @var array
27      */
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',
38             
39         'profilePath'               => 'profilepath',
40         'logonScript'               => 'scriptpath',
41         'homeDrive'                 => 'homedrive',
42         'homePath'                  => 'homedirectory',
43         
44         #'accountStatus'             => 'shadowinactive'
45     );
46
47     /**
48      * objectclasses required by this backend
49      *
50      * @var array
51      */
52     protected $_requiredObjectClass = array(
53         'top',
54         'user',
55         'person',
56         'organizationalPerson'
57     );
58
59     /**
60      * the basic group ldap filter (for example the objectclass)
61      *
62      * @var string
63      */
64     protected $_groupBaseFilter = 'objectclass=group';
65
66     /**
67      * the basic user ldap filter (for example the objectclass)
68      *
69      * @var string
70      */
71     protected $_userBaseFilter = 'objectclass=user';
72
73     protected $_isReadOnlyBackend = false;
74
75     /**
76      * the constructor
77      *
78      * @param  array  $_options  Options used in connecting, binding, etc.
79      * @throws Tinebase_Exception_Backend_Ldap
80      */
81     public function __construct(array $_options = array())
82     {
83         if(empty($_options['userUUIDAttribute'])) {
84             $_options['userUUIDAttribute']  = 'objectGUID';
85         }
86         if(empty($_options['groupUUIDAttribute'])) {
87             $_options['groupUUIDAttribute'] = 'objectGUID';
88         }
89         if(empty($_options['baseDn'])) {
90             $_options['baseDn']             = $_options['userDn'];
91         }
92         if(empty($_options['userFilter'])) {
93             $_options['userFilter']         = 'objectclass=user';
94         }
95         if(empty($_options['userSearchScope'])) {
96             $_options['userSearchScope']    = Zend_Ldap::SEARCH_SCOPE_SUB;
97         }
98         if(empty($_options['groupFilter'])) {
99             $_options['groupFilter']        = 'objectclass=group';
100         }
101         
102         parent::__construct($_options);
103         
104         if ($this->_options['useRfc2307']) {
105             $this->_requiredObjectClass[] = 'posixAccount';
106             $this->_requiredObjectClass[] = 'shadowAccount';
107             
108             $this->_rowNameMapping['accountHomeDirectory'] = 'unixhomedirectory';
109             $this->_rowNameMapping['accountLoginShell']    = 'loginshell';
110         }
111         
112         // get domain sid
113         $this->_domainConfig = $this->_ldap->search(
114             'objectClass=domain',
115             $this->_ldap->getFirstNamingContext(),
116             Zend_Ldap::SEARCH_SCOPE_BASE
117         )->getFirst();
118         
119         $this->_domainSidBinary = $this->_domainConfig['objectsid'][0];
120         $this->_domainSidPlain  = Tinebase_Ldap::decodeSid($this->_domainConfig['objectsid'][0]);
121         
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);
126     }
127     
128     /**
129      * add an user
130      * 
131      * @param   Tinebase_Model_FullUser  $_user
132      * @return  Tinebase_Model_FullUser|NULL
133      */
134     public function addUserToSyncBackend(Tinebase_Model_FullUser $_user)
135     {
136         if ($this->_isReadOnlyBackend) {
137             return NULL;
138         }
139         
140         $ldapData = $this->_user2ldap($_user);
141
142         // will be added later
143         $primaryGroupId = $ldapData['primarygroupid'];
144         unset($ldapData['primarygroupid']);
145         
146         $ldapData['objectclass'] = $this->_requiredObjectClass;
147
148         foreach ($this->_ldapPlugins as $plugin) {
149             $plugin->inspectAddUser($_user, $ldapData);
150         }
151
152         $dn = $this->_generateDn($_user);
153         
154         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
155             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  ldapData: ' . print_r($ldapData, true));
156
157         $this->_ldap->add($dn, $ldapData);
158                 
159         $userId = $this->_ldap->getEntry($dn, array($this->_userUUIDAttribute));
160         $userId = $this->_decodeAccountId($userId[$this->_userUUIDAttribute][0]);
161         
162         // add user to primary group and set primary group
163         Tinebase_Group::getInstance()->addGroupMemberInSyncBackend($_user->accountPrimaryGroup, $userId);
164         
165         // set primary group id
166         $this->_ldap->updateProperty($dn, array('primarygroupid' => $primaryGroupId));
167         
168
169         $user = $this->getUserByPropertyFromSyncBackend('accountId', $userId, 'Tinebase_Model_FullUser');
170
171         return $user;
172     }
173     
174     /**
175      * sets/unsets expiry date in ldap backend
176      *
177      * @param   mixed              $_accountId
178      * @param   Tinebase_DateTime  $_expiryDate
179      */
180     public function setExpiryDateInSyncBackend($_accountId, $_expiryDate)
181     {
182         if ($this->_isReadOnlyBackend) {
183             return;
184         }
185         
186         $metaData = $this->_getMetaData($_accountId);
187
188         if ($_expiryDate instanceof DateTime) {
189             $ldapData['accountexpires'] = bcmul(bcadd($_expiryDate->getTimestamp(), '11644473600'), '10000000');
190             
191             if ($this->_options['useRfc2307']) {
192                 // days since Jan 1, 1970
193                 $ldapData = array_merge($ldapData, array(
194                     'shadowexpire' => floor($_expiryDate->getTimestamp() / 86400)
195                 ));
196             }
197         } else {
198             $ldapData = array(
199                 'accountexpires' => '9223372036854775807'
200             );
201             
202             if ($this->_options['useRfc2307']) {
203                 $ldapData = array_merge($ldapData, array(
204                     'shadowexpire' => array()
205                 ));
206             }
207         }
208
209         foreach ($this->_ldapPlugins as $plugin) {
210             $plugin->inspectExpiryDate($_expiryDate, $ldapData);
211         }
212
213         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']}  $ldapData: " . print_r($ldapData, true));
214
215         $this->_ldap->update($metaData['dn'], $ldapData);
216     }
217     
218     /**
219      * set the password for given account
220      *
221      * @param   string  $_userId
222      * @param   string  $_password
223      * @param   bool    $_encrypt encrypt password
224      * @param   bool    $_mustChange
225      * @return  void
226      * @throws  Tinebase_Exception_InvalidArgument
227      */
228     public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
229     {
230         if ($this->_isReadOnlyBackend) {
231             return;
232         }
233         
234         $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($_userId);
235         
236         $this->checkPasswordPolicy($_password, $user);
237         
238         $metaData = $this->_getMetaData($user);
239
240         $ldapData = array(
241             'unicodePwd' => $this->_encodePassword($_password),
242         );
243         
244         if ($this->_options['useRfc2307']) {
245             $ldapData = array_merge($ldapData, array(
246                 'shadowlastchange' => floor(Tinebase_DateTime::now()->getTimestamp() / 86400)
247             ));
248         }
249         
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));
254
255         $this->_ldap->updateProperty($metaData['dn'], $ldapData);
256         
257         // update last modify timestamp in sql backend too
258         $values = array(
259             'last_password_change' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
260         );
261         
262         $where = array(
263             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
264         );
265         
266         $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
267         
268         $this->_setPluginsPassword($user->getId(), $_password, $_encrypt);
269     }
270     
271     /**
272      * update user status (enabled or disabled)
273      *
274      * @param   mixed   $_accountId
275      * @param   string  $_status
276      */
277     public function setStatusInSyncBackend($_accountId, $_status)
278     {
279         if ($this->_isReadOnlyBackend) {
280             return;
281         }
282         
283         $metaData = $this->_getMetaData($_accountId);
284         
285         if ($_status == 'enabled') {
286             $ldapData = array(
287                 'useraccountcontrol' => $metaData['useraccountcontrol'][0] &= ~self::ACCOUNTDISABLE
288             );
289             if ($this->_options['useRfc2307']) {
290                 $ldapData = array_merge($ldapData, array(
291                     'shadowMax'      => 999999,
292                     'shadowInactive' => array()
293                 ));
294             }
295         } else {
296             $ldapData = array(
297                 'useraccountcontrol' => $metaData['useraccountcontrol'][0] |=  self::ACCOUNTDISABLE
298             );
299             if ($this->_options['useRfc2307']) {
300                 $ldapData = array_merge($ldapData, array(
301                     'shadowMax'      => 1,
302                     'shadowInactive' => 1
303                 ));
304             }
305         }
306
307         foreach ($this->_ldapPlugins as $plugin) {
308             $plugin->inspectStatus($_status, $ldapData);
309         }
310
311         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
312             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']}  ldapData: " . print_r($ldapData, true));
313
314         $this->_ldap->update($metaData['dn'], $ldapData);
315     }
316     
317     /**
318      * updates an existing user
319      *
320      * @todo check required objectclasses?
321      *
322      * @param  Tinebase_Model_FullUser  $_account
323      * @return Tinebase_Model_FullUser
324      */
325     public function updateUserInSyncBackend(Tinebase_Model_FullUser $_account)
326     {
327         if ($this->_isReadOnlyBackend) {
328             return;
329         }
330         
331         Tinebase_Group::getInstance()->addGroupMemberInSyncBackend($_account->accountPrimaryGroup, $_account->getId());
332         
333         $ldapEntry = $this->_getLdapEntry('accountId', $_account);
334
335         $ldapData = $this->_user2ldap($_account, $ldapEntry);
336         
337         foreach ($this->_ldapPlugins as $plugin) {
338             $plugin->inspectUpdateUser($_account, $ldapData, $ldapEntry);
339         }
340
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);
350         }
351
352         // no need to update this attribute, it's not allowed to change and even might not be updateable
353         unset($ldapData[$this->_userUUIDAttribute]);
354
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']);
359
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));
364
365         $this->_ldap->update($ldapEntry['dn'], $ldapData);
366
367         // refetch user from ldap backend
368         $user = $this->getUserByPropertyFromSyncBackend('accountId', $_account, 'Tinebase_Model_FullUser');
369
370         return $user;
371     }
372     
373     /**
374      * convert binary id to plain text id
375      * 
376      * @param  string  $accountId
377      * @return string
378      */
379     protected function _decodeAccountId($accountId)
380     {
381         switch ($this->_userUUIDAttribute) {
382             case 'objectguid':
383                 return Tinebase_Ldap::decodeGuid($accountId);
384                 break;
385                 
386             case 'objectsid':
387                 return Tinebase_Ldap::decodeSid($accountId);
388                 break;
389
390             default:
391                 return $accountId;
392                 break;
393         }
394     }
395     
396     /**
397      * convert plain text id to binary id
398      * 
399      * @param  string  $accountId
400      * @return string
401      */
402     protected function _encodeAccountId($accountId)
403     {
404         switch ($this->_userUUIDAttribute) {
405             case 'objectguid':
406                 return Tinebase_Ldap::encodeGuid($accountId);
407                 break;
408                 
409             default:
410                 return $accountId;
411                 break;
412         }
413         
414     }
415     
416     /**
417      * generates dn for new user
418      *
419      * @param  Tinebase_Model_FullUser $_account
420      * @return string
421      */
422     protected function _generateDn(Tinebase_Model_FullUser $_account)
423     {
424         $newDn = "cn={$_account->accountFullName},{$this->_baseDn}";
425
426         return $newDn;
427     }
428     
429     /**
430      * Returns a user obj with raw data from ldap
431      *
432      * @param array $_userData
433      * @param string $_accountClass
434      * @return Tinebase_Record_Abstract
435      */
436     protected function _ldap2User(array $_userData, $_accountClass = 'Tinebase_Model_FullUser')
437     {
438         $errors = false;
439         
440         foreach ($_userData as $key => $value) {
441             if (is_int($key)) {
442                 continue;
443             }
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;
450                         } else {
451                             $accountArray[$keyMapping] = self::convertADTimestamp($value[0]);
452                         }
453                         break;
454                         
455                     case 'accountLastPasswordChange':
456                         $accountArray[$keyMapping] = self::convertADTimestamp($value[0]);
457                         break;
458                         
459                     case 'accountId':
460                         $accountArray[$keyMapping] = $this->_decodeAccountId($value[0]);
461                         
462                         break;
463                         
464                     default:
465                         $accountArray[$keyMapping] = $value[0];
466                         break;
467                 }
468             }
469         }
470
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';
474         }
475         
476         /*
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);
481             
482             if (Tinebase_DateTime::now()->compare($accountArray['accountExpires']) == -1) {
483                 $accountArray['accountStatus'] = 'disabled';
484             }
485         }*/
486
487         if (empty($accountArray['accountLastName']) && !empty($accountArray['accountFullName'])) {
488             $accountArray['accountLastName'] = $accountArray['accountFullName'];
489         }
490         
491         if ($errors) {
492             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not instantiate account object for ldap user ' . print_r($_userData, 1));
493             $accountObject = null;
494         } else {
495             $accountObject = new $_accountClass($accountArray, TRUE);
496         }
497         
498         if ($accountObject instanceof Tinebase_Model_FullUser) {
499             $accountObject->sambaSAM = new Tinebase_Model_SAMUser($accountArray);
500         }
501         
502         return $accountObject;
503     }
504
505     /**
506      * convert windows nt timestamp
507      *
508      * The timestamp is the number of 100-nanoseconds intervals (1 nanosecond = one billionth of a second)
509      *  since Jan 1, 1601 UTC.
510      *
511      * @param $timestamp
512      * @return Tinebase_DateTime
513      *
514      * @see http://www.epochconverter.com/epoch/ldap-timestamp.php
515      */
516     public static function convertADTimestamp($timestamp)
517     {
518         return new Tinebase_DateTime(bcsub(bcdiv($timestamp, '10000000'), '11644473600'));
519     }
520     
521     /**
522      * returns array of ldap data
523      *
524      * @param  Tinebase_Model_FullUser $_user
525      * @return array
526      */
527     protected function _user2ldap(Tinebase_Model_FullUser $_user, array $_ldapEntry = array())
528     {
529         $ldapData = array(
530             'useraccountcontrol' => isset($_ldapEntry['useraccountcontrol']) ? $_ldapEntry['useraccountcontrol'][0] : self::NORMAL_ACCOUNT
531         );
532         
533         foreach ($_user as $key => $value) {
534             $ldapProperty = (isset($this->_rowNameMapping[$key]) || array_key_exists($key, $this->_rowNameMapping)) ? $this->_rowNameMapping[$key] : false;
535
536             if ($ldapProperty === false) {
537                 continue;
538             }
539             
540             switch ($key) {
541                 case 'accountLastPasswordChange':
542                     // field is readOnly
543                     break;
544                     
545                 case 'accountExpires':
546                     if ($value instanceof DateTime) {
547                         $ldapData[$ldapProperty] = bcmul(bcadd($value->getTimestamp(), '11644473600'), '10000000');
548                     } else {
549                         $ldapData[$ldapProperty] = '9223372036854775807';
550                     }
551                     break;
552                     
553                 case 'accountStatus':
554                     if ($value == 'enabled') {
555                         // unset account disable flag
556                         $ldapData['useraccountcontrol'] &= ~self::ACCOUNTDISABLE;
557                     } else {
558                         // set account disable flag
559                         $ldapData['useraccountcontrol'] |=  self::ACCOUNTDISABLE;
560                     }
561                     break;
562                     
563                 case 'accountPrimaryGroup':
564                     $ldapData[$ldapProperty] = Tinebase_Group::getInstance()->resolveUUIdToGIdNumber($value);
565                     if ($this->_options['useRfc2307']) {
566                         $ldapData['gidNumber'] = Tinebase_Group::getInstance()->resolveGidNumber($value);
567                     }
568                     break;
569                     
570                 default:
571                     $ldapData[$ldapProperty] = $value;
572                     break;
573             }
574         }
575         
576         $ldapData['name'] = $ldapData['cn'];
577         $ldapData['userPrincipalName'] =  $_user->accountLoginName . '@' . $this->_domainName;
578         
579         if ($this->_options['useRfc2307']) {
580             // homedir is an required attribute
581             if (empty($ldapData['unixhomedirectory'])) {
582                 $ldapData['unixhomedirectory'] = '/dev/null';
583             }
584             
585             // set uidNumber only when not set in AD already
586             if (empty($_ldapEntry['uidnumber'])) {
587                 $ldapData['uidnumber'] = $this->_generateUidNumber();
588             }
589             $ldapData['gidnumber'] = Tinebase_Group::getInstance()->resolveGidNumber($_user->accountPrimaryGroup);
590             
591             $ldapData['msSFU30NisDomain'] = Tinebase_Helper::array_value(0, explode('.', $this->_domainName));
592         }
593         
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;
599             
600         }
601         
602         $ldapData['objectclass'] = isset($_ldapEntry['objectclass']) ? $_ldapEntry['objectclass'] : array();
603         
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));
610                 break;
611             }
612         }
613         
614         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
615             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' LDAP data ' . print_r($ldapData, true));
616         
617         return $ldapData;
618     }
619     
620     /**
621     * Encode a password to UTF-16LE
622     *
623     * @param string $password the plain password
624     * 
625     * @return string
626     */
627     protected function _encodePassword($password)
628     {
629         $password        = '"' . $password . '"';
630         $passwordLength  = strlen($password);
631         
632         $encodedPassword = null;
633
634         for ($pos = 0; $pos < $passwordLength; $pos++) {
635             $encodedPassword .= "{$password{$pos}}\000";
636         }
637         
638         return $encodedPassword;
639     }
640 }