Mereged updates from DokuWiki 38
[sudaraka-org:dokuwiki-mods.git] / inc / auth / ad.class.php
1 <?php
2 /**
3  * Active Directory authentication backend for DokuWiki
4  *
5  * This makes authentication with a Active Directory server much easier
6  * than when using the normal LDAP backend by utilizing the adLDAP library
7  *
8  * Usage:
9  *   Set DokuWiki's local.protected.php auth setting to read
10  *
11  *   $conf['useacl']         = 1;
12  *   $conf['disableactions'] = 'register';
13  *   $conf['autopasswd']     = 0;
14  *   $conf['authtype']       = 'ad';
15  *   $conf['passcrypt']      = 'ssha';
16  *
17  *   $conf['auth']['ad']['account_suffix']     = '@my.domain.org';
18  *   $conf['auth']['ad']['base_dn']            = 'DC=my,DC=domain,DC=org';
19  *   $conf['auth']['ad']['domain_controllers'] = 'srv1.domain.org,srv2.domain.org';
20  *
21  *   //optional:
22  *   $conf['auth']['ad']['sso']                = 1;
23  *   $conf['auth']['ad']['ad_username']        = 'root';
24  *   $conf['auth']['ad']['ad_password']        = 'pass';
25  *   $conf['auth']['ad']['real_primarygroup']  = 1;
26  *   $conf['auth']['ad']['use_ssl']            = 1;
27  *   $conf['auth']['ad']['use_tls']            = 1;
28  *   $conf['auth']['ad']['debug']              = 1;
29  *   // warn user about expiring password this many days in advance:
30  *   $conf['auth']['ad']['expirywarn']         = 5;
31  *
32  *   // get additional information to the userinfo array
33  *   // add a list of comma separated ldap contact fields.
34  *   $conf['auth']['ad']['additional'] = 'field1,field2';
35  *
36  * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
37  * @author  James Van Lommel <jamesvl@gmail.com>
38  * @link    http://www.nosq.com/blog/2005/08/ldap-activedirectory-and-dokuwiki/
39  * @author  Andreas Gohr <andi@splitbrain.org>
40  */
41
42 require_once(DOKU_INC.'inc/adLDAP.php');
43
44 class auth_ad extends auth_basic {
45     var $cnf = null;
46     var $opts = null;
47     var $adldap = null;
48     var $users = null;
49     var $msgshown = false;
50
51     /**
52      * Constructor
53      */
54     function __construct() {
55         global $conf;
56         $this->cnf = $conf['auth']['ad'];
57
58         // additional information fields
59         if (isset($this->cnf['additional'])) {
60             $this->cnf['additional'] = str_replace(' ', '', $this->cnf['additional']);
61             $this->cnf['additional'] = explode(',', $this->cnf['additional']);
62         } else $this->cnf['additional'] = array();
63
64         // ldap extension is needed
65         if (!function_exists('ldap_connect')) {
66             if ($this->cnf['debug'])
67                 msg("AD Auth: PHP LDAP extension not found.",-1);
68             $this->success = false;
69             return;
70         }
71
72         // Prepare SSO
73         if(!utf8_check($_SERVER['REMOTE_USER'])){
74             $_SERVER['REMOTE_USER'] = utf8_encode($_SERVER['REMOTE_USER']);
75         }
76         if($_SERVER['REMOTE_USER'] && $this->cnf['sso']){
77             // remove possible NTLM domain
78             list($dom,$usr) = explode('\\',$_SERVER['REMOTE_USER'],2);
79             if(!$usr) $usr = $dom;
80
81             // remove possible Kerberos domain
82             list($usr,$dom) = explode('@',$usr);
83
84             $dom = strtolower($dom);
85             $_SERVER['REMOTE_USER'] = $usr;
86
87             // we need to simulate a login
88             if(empty($_COOKIE[DOKU_COOKIE])){
89                 $_REQUEST['u'] = $_SERVER['REMOTE_USER'];
90                 $_REQUEST['p'] = 'sso_only';
91             }
92         }
93
94         // prepare adLDAP standard configuration
95         $this->opts = $this->cnf;
96
97         // add possible domain specific configuration
98         if($dom && is_array($this->cnf[$dom])) foreach($this->cnf[$dom] as $key => $val){
99             $this->opts[$key] = $val;
100         }
101
102         // handle multiple AD servers
103         $this->opts['domain_controllers'] = explode(',',$this->opts['domain_controllers']);
104         $this->opts['domain_controllers'] = array_map('trim',$this->opts['domain_controllers']);
105         $this->opts['domain_controllers'] = array_filter($this->opts['domain_controllers']);
106
107         // we can change the password if SSL is set
108         if($this->opts['use_ssl'] || $this->opts['use_tls']){
109             $this->cando['modPass'] = true;
110         }
111         $this->cando['modName'] = true;
112         $this->cando['modMail'] = true;
113     }
114
115     /**
116      * Check user+password [required auth function]
117      *
118      * Checks if the given user exists and the given
119      * plaintext password is correct by trying to bind
120      * to the LDAP server
121      *
122      * @author  James Van Lommel <james@nosq.com>
123      * @return  bool
124      */
125     function checkPass($user, $pass){
126         if($_SERVER['REMOTE_USER'] &&
127            $_SERVER['REMOTE_USER'] == $user &&
128            $this->cnf['sso']) return true;
129
130         if(!$this->_init()) return false;
131         return $this->adldap->authenticate($user, $pass);
132     }
133
134     /**
135      * Return user info [required auth function]
136      *
137      * Returns info about the given user needs to contain
138      * at least these fields:
139      *
140      * name string  full name of the user
141      * mail string  email address of the user
142      * grps array   list of groups the user is in
143      *
144      * This LDAP specific function returns the following
145      * addional fields:
146      *
147      * dn   string  distinguished name (DN)
148      * uid  string  Posix User ID
149      *
150      * @author  James Van Lommel <james@nosq.com>
151      */
152     function getUserData($user){
153         global $conf;
154         global $lang;
155         global $ID;
156         if(!$this->_init()) return false;
157
158         if($user == '') return array();
159
160         $fields = array('mail','displayname','samaccountname','lastpwd','pwdlastset','useraccountcontrol');
161
162         // add additional fields to read
163         $fields = array_merge($fields, $this->cnf['additional']);
164         $fields = array_unique($fields);
165
166         //get info for given user
167         $result = $this->adldap->user_info($user, $fields);
168         if($result == false){
169             return array();
170         }
171
172         //general user info
173         $info['name']    = $result[0]['displayname'][0];
174         $info['mail']    = $result[0]['mail'][0];
175         $info['uid']     = $result[0]['samaccountname'][0];
176         $info['dn']      = $result[0]['dn'];
177         //last password set (Windows counts from January 1st 1601)
178         $info['lastpwd'] = $result[0]['pwdlastset'][0] / 10000000 - 11644473600;
179         //will it expire?
180         $info['expires'] = !($result[0]['useraccountcontrol'][0] & 0x10000); //ADS_UF_DONT_EXPIRE_PASSWD
181
182         // additional information
183         foreach ($this->cnf['additional'] as $field) {
184             if (isset($result[0][strtolower($field)])) {
185                 $info[$field] = $result[0][strtolower($field)][0];
186             }
187         }
188
189         // handle ActiveDirectory memberOf
190         $info['grps'] = $this->adldap->user_groups($user,(bool) $this->opts['recursive_groups']);
191
192         if (is_array($info['grps'])) {
193             foreach ($info['grps'] as $ndx => $group) {
194                 $info['grps'][$ndx] = $this->cleanGroup($group);
195             }
196         }
197
198         // always add the default group to the list of groups
199         if(!is_array($info['grps']) || !in_array($conf['defaultgroup'],$info['grps'])){
200             $info['grps'][] = $conf['defaultgroup'];
201         }
202
203         // check expiry time
204         if($info['expires'] && $this->cnf['expirywarn']){
205             $result   = $this->adldap->domain_info(array('maxpwdage')); // maximum pass age
206             $maxage   = -1 * $result['maxpwdage'][0] / 10000000; // negative 100 nanosecs
207             $timeleft = $maxage - (time() - $info['lastpwd']);
208             $timeleft = round($timeleft/(24*60*60));
209             $info['expiresin'] = $timeleft;
210
211             // if this is the current user, warn him (once per request only)
212             if( ($_SERVER['REMOTE_USER'] == $user) &&
213                 ($timeleft <= $this->cnf['expirywarn']) &&
214                 !$this->msgshown
215             ){
216                 $msg = sprintf($lang['authpwdexpire'],$timeleft);
217                 if($this->canDo('modPass')){
218                     $url = wl($ID,array('do'=>'profile'));
219                     $msg .= ' <a href="'.$url.'">'.$lang['btn_profile'].'</a>';
220                 }
221                 msg($msg);
222                 $this->msgshown = true;
223             }
224         }
225
226         return $info;
227     }
228
229     /**
230      * Make AD group names usable by DokuWiki.
231      *
232      * Removes backslashes ('\'), pound signs ('#'), and converts spaces to underscores.
233      *
234      * @author  James Van Lommel (jamesvl@gmail.com)
235      */
236     function cleanGroup($name) {
237         $sName = str_replace('\\', '', $name);
238         $sName = str_replace('#', '', $sName);
239         $sName = preg_replace('[\s]', '_', $sName);
240         return $sName;
241     }
242
243     /**
244      * Sanitize user names
245      */
246     function cleanUser($name) {
247         return $this->cleanGroup($name);
248     }
249
250     /**
251      * Most values in LDAP are case-insensitive
252      */
253     function isCaseSensitive(){
254         return false;
255     }
256
257     /**
258      * Bulk retrieval of user data
259      *
260      * @author  Dominik Eckelmann <dokuwiki@cosmocode.de>
261      * @param   start     index of first user to be returned
262      * @param   limit     max number of users to be returned
263      * @param   filter    array of field/pattern pairs, null for no filter
264      * @return  array of userinfo (refer getUserData for internal userinfo details)
265      */
266     function retrieveUsers($start=0,$limit=-1,$filter=array()) {
267         if(!$this->_init()) return false;
268
269         if ($this->users === null) {
270             //get info for given user
271             $result = $this->adldap->all_users();
272             if (!$result) return array();
273             $this->users = array_fill_keys($result, false);
274         }
275
276         $i = 0;
277         $count = 0;
278         $this->_constructPattern($filter);
279         $result = array();
280
281         foreach ($this->users as $user => &$info) {
282             if ($i++ < $start) {
283                 continue;
284             }
285             if ($info === false) {
286                 $info = $this->getUserData($user);
287             }
288             if ($this->_filter($user, $info)) {
289                 $result[$user] = $info;
290                 if (($limit >= 0) && (++$count >= $limit)) break;
291             }
292         }
293         return $result;
294     }
295
296     /**
297      * Modify user data
298      *
299      * @param   $user      nick of the user to be changed
300      * @param   $changes   array of field/value pairs to be changed
301      * @return  bool
302      */
303     function modifyUser($user, $changes) {
304         $return = true;
305
306         // password changing
307         if(isset($changes['pass'])){
308             try {
309                 $return = $this->adldap->user_password($user,$changes['pass']);
310             } catch (adLDAPException $e) {
311                 if ($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
312                 $return = false;
313             }
314             if(!$return) msg('AD Auth: failed to change the password. Maybe the password policy was not met?',-1);
315         }
316
317         // changing user data
318         $adchanges = array();
319         if(isset($changes['name'])){
320             // get first and last name
321             $parts = explode(' ',$changes['name']);
322             $adchanges['surname']   = array_pop($parts);
323             $adchanges['firstname'] = join(' ',$parts);
324             $adchanges['display_name'] = $changes['name'];
325         }
326         if(isset($changes['mail'])){
327             $adchanges['email'] = $changes['mail'];
328         }
329         if(count($adchanges)){
330             try {
331                 $return = $return & $this->adldap->user_modify($user,$adchanges);
332             } catch (adLDAPException $e) {
333                 if ($this->cnf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
334                 $return = false;
335             }
336         }
337
338         return $return;
339     }
340
341     /**
342      * Initialize the AdLDAP library and connect to the server
343      */
344     function _init(){
345         if(!is_null($this->adldap)) return true;
346
347         // connect
348         try {
349             $this->adldap = new adLDAP($this->opts);
350             if (isset($this->opts['ad_username']) && isset($this->opts['ad_password'])) {
351                 $this->canDo['getUsers'] = true;
352             }
353             return true;
354         } catch (adLDAPException $e) {
355             if ($this->cnf['debug']) {
356                 msg('AD Auth: '.$e->getMessage(), -1);
357             }
358             $this->success = false;
359             $this->adldap  = null;
360         }
361         return false;
362     }
363
364     /**
365      * return 1 if $user + $info match $filter criteria, 0 otherwise
366      *
367      * @author   Chris Smith <chris@jalakai.co.uk>
368      */
369     function _filter($user, $info) {
370         foreach ($this->_pattern as $item => $pattern) {
371             if ($item == 'user') {
372                 if (!preg_match($pattern, $user)) return 0;
373             } else if ($item == 'grps') {
374                 if (!count(preg_grep($pattern, $info['grps']))) return 0;
375             } else {
376                 if (!preg_match($pattern, $info[$item])) return 0;
377             }
378         }
379         return 1;
380     }
381
382     function _constructPattern($filter) {
383         $this->_pattern = array();
384         foreach ($filter as $item => $pattern) {
385             $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i';    // allow regex characters
386         }
387     }
388 }
389
390 //Setup VIM: ex: et ts=4 :