Merge branch '2013.10' into 2014.11
[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         Zend_Ldap_Dn::explodeDn($this->_domainConfig['distinguishedname'][0], $fooBar, $domanNameParts);
124         $this->_domainName = implode('.', $domanNameParts);
125     }
126     
127     /**
128      * add an user
129      * 
130      * @param   Tinebase_Model_FullUser  $_user
131      * @return  Tinebase_Model_FullUser|NULL
132      */
133     public function addUserToSyncBackend(Tinebase_Model_FullUser $_user)
134     {
135         if ($this->_isReadOnlyBackend) {
136             return NULL;
137         }
138         
139         $ldapData = $this->_user2ldap($_user);
140
141         // will be added later
142         $primaryGroupId = $ldapData['primarygroupid'];
143         unset($ldapData['primarygroupid']);
144         
145         $ldapData['objectclass'] = $this->_requiredObjectClass;
146
147         foreach ($this->_ldapPlugins as $plugin) {
148             $plugin->inspectAddUser($_user, $ldapData);
149         }
150
151         $dn = $this->generateDn($_user);
152         
153         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
154             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  ldapData: ' . print_r($ldapData, true));
155
156         $this->_ldap->add($dn, $ldapData);
157                 
158         $userId = $this->_ldap->getEntry($dn, array($this->_userUUIDAttribute));
159         $userId = $this->_decodeAccountId($userId[$this->_userUUIDAttribute][0]);
160         
161         // add user to primary group and set primary group
162         Tinebase_Group::getInstance()->addGroupMemberInSyncBackend($_user->accountPrimaryGroup, $userId);
163         
164         // set primary group id
165         $this->_ldap->updateProperty($dn, array('primarygroupid' => $primaryGroupId));
166         
167
168         $user = $this->getUserByPropertyFromSyncBackend('accountId', $userId, 'Tinebase_Model_FullUser');
169
170         return $user;
171     }
172     
173     /**
174      * sets/unsets expiry date in ldap backend
175      *
176      * @param   mixed              $_accountId
177      * @param   Tinebase_DateTime  $_expiryDate
178      */
179     public function setExpiryDateInSyncBackend($_accountId, $_expiryDate)
180     {
181         if ($this->_isReadOnlyBackend) {
182             return;
183         }
184         
185         $metaData = $this->_getMetaData($_accountId);
186
187         if ($_expiryDate instanceof DateTime) {
188             $ldapData['accountexpires'] = bcmul(bcadd($_expiryDate->getTimestamp(), '11644473600'), '10000000');
189             
190             if ($this->_options['useRfc2307']) {
191                 // days since Jan 1, 1970
192                 $ldapData = array_merge($ldapData, array(
193                     'shadowexpire' => floor($_expiryDate->getTimestamp() / 86400)
194                 ));
195             }
196         } else {
197             $ldapData = array(
198                 'accountexpires' => '9223372036854775807'
199             );
200             
201             if ($this->_options['useRfc2307']) {
202                 $ldapData = array_merge($ldapData, array(
203                     'shadowexpire' => array()
204                 ));
205             }
206         }
207
208         foreach ($this->_ldapPlugins as $plugin) {
209             $plugin->inspectExpiryDate($_expiryDate, $ldapData);
210         }
211
212         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']}  $ldapData: " . print_r($ldapData, true));
213
214         $this->_ldap->update($metaData['dn'], $ldapData);
215     }
216     
217     /**
218      * set the password for given account
219      *
220      * @param   string  $_userId
221      * @param   string  $_password
222      * @param   bool    $_encrypt encrypt password
223      * @param   bool    $_mustChange
224      * @return  void
225      * @throws  Tinebase_Exception_InvalidArgument
226      */
227     public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
228     {
229         if ($this->_isReadOnlyBackend) {
230             return;
231         }
232         
233         $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($_userId);
234         
235         $this->checkPasswordPolicy($_password, $user);
236         
237         $metaData = $this->_getMetaData($user);
238
239         $ldapData = array(
240             'unicodePwd' => $this->_encodePassword($_password),
241         );
242         
243         if ($this->_options['useRfc2307']) {
244             $ldapData = array_merge($ldapData, array(
245                 'shadowlastchange' => floor(Tinebase_DateTime::now()->getTimestamp() / 86400)
246             ));
247         }
248         
249         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
250             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $metaData['dn']);
251         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
252             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
253
254         $this->_ldap->updateProperty($metaData['dn'], $ldapData);
255         
256         // update last modify timestamp in sql backend too
257         $values = array(
258             'last_password_change' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
259         );
260         
261         $where = array(
262             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
263         );
264         
265         $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
266         
267         $this->_setPluginsPassword($user->getId(), $_password, $_encrypt);
268     }
269     
270     /**
271      * update user status (enabled or disabled)
272      *
273      * @param   mixed   $_accountId
274      * @param   string  $_status
275      */
276     public function setStatusInSyncBackend($_accountId, $_status)
277     {
278         if ($this->_isReadOnlyBackend) {
279             return;
280         }
281         
282         $metaData = $this->_getMetaData($_accountId);
283         
284         if ($_status == 'enabled') {
285             $ldapData = array(
286                 'useraccountcontrol' => $metaData['useraccountcontrol'][0] &= ~self::ACCOUNTDISABLE
287             );
288             if ($this->_options['useRfc2307']) {
289                 $ldapData = array_merge($ldapData, array(
290                     'shadowMax'      => 999999,
291                     'shadowInactive' => array()
292                 ));
293             }
294         } else {
295             $ldapData = array(
296                 'useraccountcontrol' => $metaData['useraccountcontrol'][0] |=  self::ACCOUNTDISABLE
297             );
298             if ($this->_options['useRfc2307']) {
299                 $ldapData = array_merge($ldapData, array(
300                     'shadowMax'      => 1,
301                     'shadowInactive' => 1
302                 ));
303             }
304         }
305
306         foreach ($this->_ldapPlugins as $plugin) {
307             $plugin->inspectStatus($_status, $ldapData);
308         }
309
310         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
311             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']}  ldapData: " . print_r($ldapData, true));
312
313         $this->_ldap->update($metaData['dn'], $ldapData);
314     }
315     
316     /**
317      * updates an existing user
318      *
319      * @todo check required objectclasses?
320      *
321      * @param  Tinebase_Model_FullUser  $_account
322      * @return Tinebase_Model_FullUser
323      */
324     public function updateUserInSyncBackend(Tinebase_Model_FullUser $_account)
325     {
326         if ($this->_isReadOnlyBackend) {
327             return;
328         }
329         
330         Tinebase_Group::getInstance()->addGroupMemberInSyncBackend($_account->accountPrimaryGroup, $_account->getId());
331         
332         $ldapEntry = $this->_getLdapEntry('accountId', $_account);
333
334         $ldapData = $this->_user2ldap($_account, $ldapEntry);
335         
336         foreach ($this->_ldapPlugins as $plugin) {
337             $plugin->inspectUpdateUser($_account, $ldapData, $ldapEntry);
338         }
339
340         // no need to update this attribute, it's not allowed to change and even might not be updateable
341         unset($ldapData[$this->_userUUIDAttribute]);
342
343         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
344             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $ldapEntry['dn']);
345         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
346             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
347
348         $this->_ldap->update($ldapEntry['dn'], $ldapData);
349         
350         $dn = Zend_Ldap_Dn::factory($ldapEntry['dn'], null);
351         $rdn = $dn->getRdn();
352         
353         // do we need to rename the entry?
354         if ($rdn['CN'] != $ldapData['cn']) {
355             $newDN = $this->generateDn($_account);
356             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
357                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  rename ldap entry to: ' . $newDN);
358             $this->_ldap->rename($dn, $newDN);
359         }
360         
361         // refetch user from ldap backend
362         $user = $this->getUserByPropertyFromSyncBackend('accountId', $_account, 'Tinebase_Model_FullUser');
363
364         return $user;
365     }
366     
367     /**
368      * convert binary id to plain text id
369      * 
370      * @param  string  $accountId
371      * @return string
372      */
373     protected function _decodeAccountId($accountId)
374     {
375         switch ($this->_userUUIDAttribute) {
376             case 'objectguid':
377                 return Tinebase_Ldap::decodeGuid($accountId);
378                 break;
379                 
380             case 'objectsid':
381                 return Tinebase_Ldap::decodeSid($accountId);
382                 break;
383                 
384             default:
385                 return $accountId;
386                 break;
387         }
388         
389     }
390     
391     /**
392      * convert plain text id to binary id
393      * 
394      * @param  string  $accountId
395      * @return string
396      */
397     protected function _encodeAccountId($accountId)
398     {
399         switch ($this->_userUUIDAttribute) {
400             case 'objectguid':
401                 return Tinebase_Ldap::encodeGuid($accountId);
402                 break;
403                 
404             default:
405                 return $accountId;
406                 break;
407         }
408         
409     }
410     
411     /**
412      * generates dn for new user
413      *
414      * @param  Tinebase_Model_FullUser $_account
415      * @return string
416      */
417     public function generateDn(Tinebase_Model_FullUser $_account)
418     {
419         $newDn = "cn={$_account->accountFullName},{$this->_baseDn}";
420
421         return $newDn;
422     }
423     
424     /**
425      * Returns a user obj with raw data from ldap
426      *
427      * @param array $_userData
428      * @param string $_accountClass
429      * @return Tinebase_Record_Abstract
430      */
431     protected function _ldap2User(array $_userData, $_accountClass)
432     {
433         $errors = false;
434         
435         foreach ($_userData as $key => $value) {
436             if (is_int($key)) {
437                 continue;
438             }
439             $keyMapping = array_search($key, $this->_rowNameMapping);
440             if ($keyMapping !== FALSE) {
441                 switch($keyMapping) {
442                     case 'accountExpires':
443                         if ($value === '0' || $value[0] === '9223372036854775807') {
444                             $accountArray[$keyMapping] = null;
445                         } else {
446                             $accountArray[$keyMapping] = new Tinebase_DateTime(bcsub(bcdiv($value[0], '10000000'), '11644473600'));
447                         }
448                         break;
449                         
450                     case 'accountLastPasswordChange':
451                         $accountArray[$keyMapping] = new Tinebase_DateTime(bcsub(bcdiv($value[0], '10000000'), '11644473600'));
452                         break;
453                         
454                     case 'accountId':
455                         $accountArray[$keyMapping] = $this->_decodeAccountId($value[0]);
456                         
457                         break;
458                         
459                     default:
460                         $accountArray[$keyMapping] = $value[0];
461                         break;
462                 }
463             }
464         }
465
466         $accountArray['accountStatus'] = (isset($_userData['useraccountcontrol']) && ($_userData['useraccountcontrol'][0] & self::ACCOUNTDISABLE)) ? 'disabled' : 'enabled';
467         if ($accountArray['accountExpires'] instanceof Tinebase_DateTime && Tinebase_DateTime::now()->compare($accountArray['accountExpires']) == -1) {
468             $accountArray['accountStatus'] = 'disabled';
469         }
470         
471         /*
472         $maxPasswordAge = abs(bcdiv($this->_domainConfig['maxpwdage'][0], '10000000'));
473         if ($maxPasswordAge > 0 && isset($accountArray['accountLastPasswordChange'])) {
474             $accountArray['accountExpires'] = clone $accountArray['accountLastPasswordChange'];
475             $accountArray['accountExpires']->addSecond($maxPasswordAge);
476             
477             if (Tinebase_DateTime::now()->compare($accountArray['accountExpires']) == -1) {
478                 $accountArray['accountStatus'] = 'disabled';
479             }
480         }*/
481
482         if (empty($accountArray['accountLastName']) && !empty($accountArray['accountFullName'])) {
483             $accountArray['accountLastName'] = $accountArray['accountFullName'];
484         }
485         
486         if ($errors) {
487             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not instantiate account object for ldap user ' . print_r($_userData, 1));
488             $accountObject = null;
489         } else {
490             $accountObject = new $_accountClass($accountArray, TRUE);
491         }
492         
493         if ($accountObject instanceof Tinebase_Model_FullUser) {
494             $accountObject->sambaSAM = new Tinebase_Model_SAMUser($accountArray);
495         }
496         
497         return $accountObject;
498     }
499     
500     /**
501      * returns array of ldap data
502      *
503      * @param  Tinebase_Model_FullUser $_user
504      * @return array
505      */
506     protected function _user2ldap(Tinebase_Model_FullUser $_user, array $_ldapEntry = array())
507     {
508         $ldapData = array(
509             'useraccountcontrol' => isset($_ldapEntry['useraccountcontrol']) ? $_ldapEntry['useraccountcontrol'][0] : self::NORMAL_ACCOUNT
510         );
511         
512         foreach ($_user as $key => $value) {
513             $ldapProperty = (isset($this->_rowNameMapping[$key]) || array_key_exists($key, $this->_rowNameMapping)) ? $this->_rowNameMapping[$key] : false;
514
515             if ($ldapProperty === false) {
516                 continue;
517             }
518             
519             switch ($key) {
520                 case 'accountLastPasswordChange':
521                     // field is readOnly
522                     break;
523                     
524                 case 'accountExpires':
525                     if ($value instanceof DateTime) {
526                         $ldapData[$ldapProperty] = bcmul(bcadd($value->getTimestamp(), '11644473600'), '10000000');
527                     } else {
528                         $ldapData[$ldapProperty] = '9223372036854775807';
529                     }
530                     break;
531                     
532                 case 'accountStatus':
533                     if ($value == 'enabled') {
534                         // unset account disable flag
535                         $ldapData['useraccountcontrol'] &= ~self::ACCOUNTDISABLE;
536                     } else {
537                         // set account disable flag
538                         $ldapData['useraccountcontrol'] |=  self::ACCOUNTDISABLE;
539                     }
540                     break;
541                     
542                 case 'accountPrimaryGroup':
543                     $ldapData[$ldapProperty] = Tinebase_Group::getInstance()->resolveUUIdToGIdNumber($value);
544                     if ($this->_options['useRfc2307']) {
545                         $ldapData['gidNumber'] = Tinebase_Group::getInstance()->resolveGidNumber($value);
546                     }
547                     break;
548                     
549                 default:
550                     $ldapData[$ldapProperty] = $value;
551                     break;
552             }
553         }
554         
555         $ldapData['name'] = $ldapData['cn'];
556         $ldapData['userPrincipalName'] =  $_user->accountLoginName . '@' . $this->_domainName;
557         
558         if ($this->_options['useRfc2307']) {
559             // homedir is an required attribute
560             if (empty($ldapData['unixhomedirectory'])) {
561                 $ldapData['unixhomedirectory'] = '/dev/null';
562             }
563             
564             // set uidNumber only when not set in AD already
565             if (empty($_ldapEntry['uidnumber'])) {
566                 $ldapData['uidnumber'] = $this->_generateUidNumber();
567             }
568             $ldapData['gidnumber'] = Tinebase_Group::getInstance()->resolveGidNumber($_user->accountPrimaryGroup);
569             
570             $ldapData['msSFU30NisDomain'] = Tinebase_Helper::array_value(0, explode('.', $this->_domainName));
571         }
572         
573         if (isset($_user->sambaSAM) && $_user->sambaSAM instanceof Tinebase_Model_SAMUser) {
574             $ldapData['profilepath']   = $_user->sambaSAM->profilePath;
575             $ldapData['scriptpath']    = $_user->sambaSAM->logonScript;
576             $ldapData['homedrive']     = $_user->sambaSAM->homeDrive;
577             $ldapData['homedirectory'] = $_user->sambaSAM->homePath;
578             
579         }
580         
581         $ldapData['objectclass'] = isset($_ldapEntry['objectclass']) ? $_ldapEntry['objectclass'] : array();
582         
583         // check if user has all required object classes. This is needed
584         // when updating users which where created using different requirements
585         foreach ($this->_requiredObjectClass as $className) {
586             if (! in_array($className, $ldapData['objectclass'])) {
587                 // merge all required classes at once
588                 $ldapData['objectclass'] = array_unique(array_merge($ldapData['objectclass'], $this->_requiredObjectClass));
589                 break;
590             }
591         }
592         
593         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
594             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' LDAP data ' . print_r($ldapData, true));
595         
596         return $ldapData;
597     }
598     
599     /**
600     * Encode a password to UTF-16LE
601     *
602     * @param string $password the plain password
603     * 
604     * @return string
605     */
606     protected function _encodePassword($password)
607     {
608         $password        = '"' . $password . '"';
609         $passwordLength  = strlen($password);
610         
611         $encodedPassword = null;
612
613         for ($pos = 0; $pos < $passwordLength; $pos++) {
614             $encodedPassword .= "{$password{$pos}}\000";
615         }
616         
617         return $encodedPassword;
618     }
619 }