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