1<?php
2/**
3 * DokuWiki Plugin authsplit (Auth Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Pieter Hollants <pieter@hollants.com>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12class auth_plugin_authsplit extends DokuWiki_Auth_Plugin {
13    protected $authplugins;
14    protected $autocreate_users;
15    protected $debug;
16
17    /**
18     * Show a debug message
19     *
20     * @param  string $message  The message to show
21     * @param  int    $err      -1 for error, 0 for info, 1 for success
22     * @param  int    $line     The line in $file that triggered the message.
23     * @param  string $file     The filename that triggered the message.
24     * @return void
25     */
26    protected function _debug($message, $err, $line, $file) {
27        if (!$this->debug)
28            return;
29        msg($message, $err, $line, $file);
30    }
31
32    /**
33     * Constructor.
34     */
35    public function __construct() {
36        parent::__construct(); // for compatibility
37
38        /* Load the config earlier than usual (we need it below) */
39        $this->loadConfig();
40
41        /* Load all referenced auth plugins */
42        foreach (array('primary', 'secondary') as $type) {
43            $settingName = $type.'_authplugin';
44            $pluginName = $this->getConf($settingName);
45            if (!$pluginName) {
46                msg(sprintf($this->getLang('nocfg'), $settingName), -1);
47                $this->success = false;
48                return;
49            }
50            if ($pluginName != 'None') {
51                $this->authplugins[$type] = plugin_load('auth', $pluginName);
52                if (!$this->authplugins[$type]) {
53                    msg(sprintf($this->getLang('pluginload'), $pluginName), -1);
54                    $this->success = false;
55                    return;
56                }
57            } else {
58                $this->authplugins[$type] = null;
59            }
60        }
61
62        /* Create users automatically? */
63        $this->autocreate_users = $this->getConf('autocreate_users', null);
64        if ($this->autocreate_users === null) {
65            msg(sprintf($this->getLang('nocfg'), 'autocreate_users'), -1);
66            $this->success = false;
67            return;
68        }
69
70        /* Convert username's case? */
71        $this->username_caseconversion = $this->getConf('username_caseconversion', null);
72        if ($this->username_caseconversion === null) {
73            msg(sprintf($this->getLang('nocfg'), 'username_caseconversion'), -1);
74            $this->success = false;
75            return;
76        }
77
78        /* Show debug messages? */
79        $this->debug = $this->getConf('debug', null);
80        if ($this->debug === null) {
81            msg(sprintf($this->getLang('nocfg'), 'debug'), -1);
82            $this->success = false;
83            return;
84        }
85
86        /* Of course, to modify login names actually BOTH auth plugins must
87           support that. However, at this place we just consider the secondary
88           auth plugin as otherwise admins can not add user accounts there in
89           advance. */
90        $this->cando['modLogin'] = $this->authplugins['secondary']->canDo('modLogin');
91
92        /* To modify passwords, the primary auth plugin must support it */
93        $this->cando['modPass'] = $this->authplugins['primary']->canDo('modPass');
94
95        /* To add and delete user accounts, modify real names, email addresses,
96           group memberships and groups, it is sufficient for the secondary
97           auth plugin to support it. */
98        foreach (array('addUser', 'delUser', 'modName', 'modMail', 'modGroups',
99                       'getUsers', 'getUserCount', 'getGroups') as $cap) {
100            $this->cando[$cap] = $this->authplugins['secondary']->canDo($cap);
101        }
102
103        /* The primary auth plugin determines whether we do external
104           authentication (eg. against a third party cookie). */
105        $this->cando['external'] = $this->authplugins['primary']->cando['external'];
106
107        /* Whether we can do logout or not depends on the primary auth plugin */
108        $this->cando['logout'] = $this->authplugins['primary']->canDo('logout');
109
110        $msg = 'authsplit:__construct(): '.
111               $this->authplugins['primary']->getPluginName().'/'.
112               $this->authplugins['secondary']->getPluginName().' '.
113               'combination can ';
114        $parts = array(
115            'addUser'      => 'add users',
116            'delUser'      => 'delete users',
117            'modLogin'     => 'modify login names',
118            'modPass'      => 'modify passwords',
119            'modName'      => 'modify real names',
120            'modMail'      => 'modify E-Mail addresses',
121            'modGroups'    => 'modify groups',
122            'getUsers'     => 'get user list',
123            'getUserCount' => 'get user counts',
124            'getGroups'    => 'get groups',
125            'logout'       => 'logout users',
126        );
127        foreach ($this->cando as $key => $value) {
128            if ($this->cando[$key])
129                $msg .= $parts[$key].', ';
130        }
131        $msg = rtrim($msg, ', ').'.';
132        $this->_debug($msg, 1, __LINE__, __FILE__);
133    }
134
135    /**
136     * Authenticate user (DokuWiki-style user/password authentication)
137     *
138     * This method is called if DokuWiki's internal authentication system is
139     * used (primary auth plugin's canDo['external'] == false) and the user
140     * supplied a username and a password.
141     *
142     * @param   string $user the user name
143     * @param   string $pass the clear text password
144     * @return  bool
145     */
146    public function checkPass($user, $pass) {
147        /* First validate the username and password with the primary plugin. */
148        if (!$this->authplugins['primary']->checkPass($user, $pass)) {
149            $this->_debug(
150                'authsplit:checkPass(): primary auth plugin\'s checkPass() '.
151                'failed', -1, __LINE__, __FILE__
152            );
153            return false;
154        }
155        $this->_debug(
156            'authsplit:checkPass(): primary auth plugin authenticated the '.
157            'user successfully.', 1, __LINE__, __FILE__
158        );
159
160        /* Then make sure that the secondary auth plugin also knows about the
161           user. */
162        return $this->_checkUserOnSecondaryAuthPlugin($user);
163    }
164
165    /**
166     * Authenticate user (external authentication)
167     *
168     * This method is called on every page load if external authentication is
169     * used (primary auth plugin's canDo['external'] == true).
170     *
171     * @param   string $user   The user name (may be empty)
172     * @param   string $pass   The clear text password (may be empty)
173     * @param   bool   $sticky Whether the cookie should not expire
174     * @return  bool
175     */
176    public function trustExternal($user, $pass, $sticky = false) {
177        global $USERINFO;
178
179        /* First delegate to the primary plugin's trustExternal() to
180           validate the user (by means of a password, a cookie or whatever). */
181        if (!$this->authplugins['primary']->trustExternal($user, $pass, $sticky)) {
182            $this->_debug(
183                'authsplit:trustExternal(): primary auth plugin\'s ' .
184                'trustExternal() failed', -1, __LINE__, __FILE__
185            );
186            return false;
187        }
188
189        /* As $user may be empty (eg. when authentication is done via a cookie)
190           we set it from the server environment */
191        $user = $_SERVER['REMOTE_USER'];
192        $this->_debug(
193            'authsplit:trustExternal(): derived user name: ' . $user,
194            -1, __LINE__, __FILE__
195        );
196
197        /* Then make sure the secondary auth plugin also knows about the
198           user. */
199        if ($this->_checkUserOnSecondaryAuthPlugin($user)) {
200            /* With external authentication, $USERINFO must be set on every
201               page load. */
202            $USERINFO = $this->getUserData($user, true);
203            return true;
204        }
205        return false;
206    }
207
208    /**
209     * Ensures that the user is known to the secondary auth plugin as well.
210     *
211     * @param   string $user the user name
212     * @return  bool
213     */
214    public function _checkUserOnSecondaryAuthPlugin($user) {
215        /* User already known? */
216        $userinfo = $this->authplugins['secondary']->getUserData($user, false);
217        if ($userinfo)
218            return true;
219
220        $this->_debug(
221            'authsplit:'.__FUNCTION__.'(): secondary auth plugin\'s ' .
222            'getUserData() failed, seems user is yet unknown there.', -1,
223            __LINE__, __FILE__
224        );
225
226        $this->_debug(
227            'authsplit:'.__FUNCTION__.'(): autocreate_users is set to '.
228            $this->autocreate_users.'.',
229            $this->autocreate_users == 1 ? 1 : -1,
230            __LINE__, __FILE__
231        );
232
233        /* Make sure automatic user creation is enabled */
234        if (!$this->autocreate_users)
235            return false;
236
237        /* Make sure the secondary auth plugin can create user accounts */
238        if (!$this->authplugins['secondary']->cando['addUser']) {
239            msg(
240                sprintf(
241                    $this->getLang('erraddusercap'),
242                    $this->authplugins['secondary']->getPluginName()
243                ),
244                -1
245            );
246            return false;
247        }
248
249        /* Since auth plugins by definition must have a getUserData()
250           method, we use the primary auth plugin's data to create a user
251           account in the secondary auth plugin. */
252        $params = $this->authplugins['primary']->getUserData($user, true);
253        if (!$params) {
254            msg(
255                sprintf(
256                    $this->getLang('erradduserinfo'),
257                    $this->authplugins['primary']->getPluginName()
258                ),
259                -1
260            );
261            return false;
262        }
263        $this->_debug(
264            'authsplit:'.__FUNCTION__.'(): primary auth plugin\'s ' .
265            'getUserData(): '.$this->_dumpUserData($params).'.',
266            1, __LINE__, __FILE__
267        );
268
269        /* Create the new user account */
270        $result = $this->triggerUserMod(
271            'create',
272            array(
273                $user, $pass,
274                $params['name'], $params['mail'], $params['grps']
275            )
276        );
277        if ($result === false || $result === null)
278        {
279            $this->_debug(
280                'authsplit:'.__FUNCTION__.'(): primary auth plugin\'s '.
281                'getUserData() could not supply data.', -1,
282                __LINE__, __FILE__
283            );
284            return false;
285        }
286        if(actionOK('profile'))
287        {
288            msg($this->getLang('autocreated'), -1);
289        }
290
291        return true;
292    }
293
294    /**
295     * Logoff the user (useful for external authentication)
296     *
297     * @param   string $user the user name
298     * @param   string $pass the clear text password
299     * @return  bool
300     */
301    public function logOff() {
302        return $this->authplugins['primary']->logOff();
303    }
304
305    /**
306     * Log the user In (useful for external authentication)
307     *
308     * @return  bool
309     */
310    public function logIn() {
311        return $this->authplugins['primary']->logIn();
312    }
313
314    /**
315     * Return user info
316     *
317     * Returned info about the given user needs to contain
318     * at least these fields:
319     *
320     * name string  full name of the user
321     * mail string  email address of the user
322     * grps array   list of groups the user is in
323     *
324     * @param   string $user the user name
325     * @param   bool $requireGroups whether to return user's groups as well
326     * @return  array containing user data or false
327     */
328    public function getUserData($user, $requireGroups = true) {
329        /* A user must be present in BOTH auth plugins. */
330        $userinfo = $this->authplugins['primary']->getUserData($user, false);
331        if (!$userinfo) {
332            $this->_debug(
333                'authsplit:checkPass(): primary auth plugin\'s getUserData() '.
334                'failed, seems user is yet unknown there.', 1,
335                __LINE__, __FILE__
336            );
337            return false;
338        }
339
340        $userinfo = $this->authplugins['secondary']->getUserData($user, $requireGroups);
341        if (!$userinfo) {
342            $this->_debug(
343                'authsplit:checkPass(): secondary auth plugin\'s getUserData() '.
344                'failed, seems user is yet unknown there.', 1,
345                __LINE__, __FILE__
346            );
347            return false;
348        }
349        $this->_debug(
350            'authsplit:getUserData(): secondary auth plugin\'s getUserData(): '.
351            $this->_dumpUserData($userinfo).'.', 1, __LINE__, __FILE__
352        );
353
354        return $userinfo;
355    }
356
357    /**
358     * Returns a string representation of user data for debugging purposes
359     *
360     * @param  array  $user An array with user data
361     * @return string
362     */
363    protected function _dumpUserData($user) {
364        $msg = 'Name: "'.$user['name'].'", '.
365               'Mail: "'.$user['mail'].'", ' .
366               'Groups: ';
367        foreach ($user['grps'] as $grp) {
368            $msg .= '"'.$grp.'", ';
369        }
370        $msg = rtrim($msg, ', ');
371        return $msg;
372    }
373
374    /**
375     * Create a new User
376     *
377     * Returns false if the user already exists, null when an error
378     * occurred and true if everything went well.
379     *
380     * The new user HAS TO be added to the default group by this
381     * function!
382     *
383     * Set addUser capability when implemented
384     *
385     * @param  string     $user
386     * @param  string     $pass
387     * @param  string     $name
388     * @param  string     $mail
389     * @param  null|array $grps
390     * @return bool|null
391     */
392    public function createUser($user, $pass, $name, $mail, $grps = null) {
393        /* Does the user not exist yet in the primary auth plugin and does it
394           support creating users? */
395        $userinfo = $this->authplugins['primary']->getUserData($user, false);
396        if (!$userinfo && $this->authplugins['primary']->cando['addUser']) {
397            $result = $this->authplugins['primary']->createUser(
398                $user, $pass, $name, $email
399            );
400            if ($result === false || $result === null) {
401                $this->_debug(
402                    'authsplit:createUser(): primary auth plugin\'s '.
403                    'createUser() failed.', -1, __LINE__, __FILE__
404                );
405                return $result;
406            }
407            $this->_debug(
408                'authsplit:createUser(): user created in primary auth plugin.',
409                1, __LINE__, __FILE__
410            );
411        }
412
413        /* We need to create the user in the secondary auth plugin in any case. */
414        $result = $this->authplugins['secondary']->createUser(
415            $user, '', $name, $mail, $grps
416        );
417        if ($result === false || $result === null) {
418            $this->_debug(
419                'authsplit:createUser(): secondary auth plugin\'s '.
420                'createUser() failed.', -1, __LINE__, __FILE__
421            );
422            return $result;
423        }
424
425        $this->_debug(
426            'authsplit:createUser(): user created in secondary auth plugin.',
427            1, __LINE__, __FILE__
428        );
429
430        return true;
431    }
432
433    /**
434     * Modify user data
435     *
436     * Set the mod* capabilities according to the implemented features
437     *
438     * @param   string $user    nick of the user to be changed
439     * @param   array  $changes array of field/value pairs to be changed
440     *                          (password will be clear text)
441     * @return  bool
442     */
443    public function modifyUser($user, $changes) {
444        if (!is_array($changes) || !count($changes))
445            return true; // nothing to change
446
447        foreach ($changes as $field => $value) {
448            if ($field == 'pass') {
449                /* Passwords must be changed in the primary auth plugin */
450                $result = $this->authplugins['primary']->modifyUser(
451                    $user,
452                    array(
453                        'pass' => $value
454                    )
455                );
456                if (!$result) {
457                    $this->_debug(
458                        'authsplit:modifyUser(): primary auth plugin\'s '.
459                        'modifyUser() failed.', -1, __LINE__, __FILE__
460                    );
461                    return false;
462                }
463            }
464            elseif ($field == 'grps') {
465                /* Groups are handled by the secondary auth plugin. */
466                $result = $this->authplugins['secondary']->modifyUser(
467                    $user,
468                    array(
469                        'grps' => $value
470                    )
471                );
472                if (!$result) {
473                    $this->_debug(
474                        'authsplit:modifyUser(): secondary auth plugin\'s '.
475                        'modifyUser() failed.', -1, __LINE__, __FILE__
476                    );
477                    return false;
478                }
479            }
480            elseif ( ($field == 'login') || ($field == 'name') || ($field == 'mail') ) {
481                /* If the primary auth plugin supports the update,
482                   we'll try it there first. */
483                if ($this->authplugins['primary']->canDo['mod' . ucfirst($field)]) {
484                    $result = $this->authplugins['primary']->modifyUser(
485                        $user, array(
486                            $field => $value
487                        )
488                    );
489                    if (!$result) {
490                        $this->_debug(
491                            'authsplit:modifyUser(): primary auth plugin\'s '.
492                            'modifyUser() failed.', -1, __LINE__, __FILE__
493                        );
494                        return false;
495                    }
496                }
497
498                /* Now in the secondary auth plugin. */
499                $result = $this->authplugins['secondary']->modifyUser(
500                    $user,
501                    array(
502                        $field => $value
503                    )
504                );
505                if (!$result) {
506                    $this->_debug(
507                        'authsplit:modifyUser(): secondary auth plugin\'s '.
508                        'modifyUser() failed.', -1, __LINE__, __FILE__
509                    );
510                    return false;
511                }
512            }
513        }
514
515        return true;
516    }
517
518    /**
519     * Delete one or more users
520     *
521     * Set delUser capability when implemented
522     *
523     * @param   array  $users
524     * @return  int    number of users deleted
525     */
526    public function deleteUsers($users) {
527        /* We do NOT attempt to delete the user with the primary auth plugin.
528           Just because we don't want the account in DokuWiki any more, does not
529           mean that there are no other services that depend on the account's
530           existance in the primary auth source. */
531        $result = $this->authplugins['secondary']->deleteUsers($users);
532        if (!$result) {
533            $this->_debug(
534                'authsplit:deleteUsers(): secondary auth plugin\'s '.
535                'deleteUsers() failed.', -1, __LINE__, __FILE__
536            );
537        }
538        return $result;
539    }
540
541    /**
542     * Bulk retrieval of user data
543     *
544     * Set getUsers capability when implemented
545     *
546     * @param   int   $start     index of first user to be returned
547     * @param   int   $limit     max number of users to be returned
548     * @param   array $filter    array of field/pattern pairs, null for no filter
549     * @return  array list of userinfo (refer to getuseraccts for internal
550     *                userinfo details)
551     */
552    public function retrieveUsers($start = 0, $limit = -1, $filter = null) {
553        /* We're always interested in the users defined in the secondary auth
554           plugin. */
555        $result = $this->authplugins['secondary']->retrieveUsers(
556            $start, $limit, $filter
557        );
558        if (!$result) {
559            $this->_debug(
560                'authsplit:retrieveUsers(): secondary auth plugin\'s '.
561                'retrieveUsers() failed.', -1, __LINE__, __FILE__
562            );
563        }
564        return $result;
565    }
566
567    /**
568     * Return a count of the number of user which meet $filter criteria
569     *
570     * Set getUserCount capability when implemented
571     *
572     * @param  array $filter array of field/pattern pairs, empty array for no filter
573     * @return int
574     */
575    public function getUserCount($filter = array()) {
576        /* We're always interested in the users defined in the secondary auth plugin. */
577        $result = $this->authplugins['secondary']->getUserCount($filter);
578        if (!$result) {
579            $this->_debug(
580                'authsplit:getUserCount(): secondary auth plugin\'s '.
581                'getUserCount() failed.', -1, __LINE__, __FILE__
582            );
583        }
584        return $result;
585    }
586
587    /**
588     * Define a group
589     *
590     * Set addGroup capability when implemented
591     *
592     * @param   string $group
593     * @return  bool
594     */
595    public function addGroup($group) {
596        /* Groups are always defined in the secondary auth plugin. */
597        $result = $this->authplugins['secondary']->addGroup($group);
598        if (!$result) {
599            $this->_debug(
600                'authsplit:addGroup(): secondary auth plugin\'s addGroup() '.
601                'failed.', -1, __LINE__, __FILE__
602            );
603        }
604        return $result;
605    }
606
607    /**
608     * Retrieve groups
609     *
610     * Set getGroups capability when implemented
611     *
612     * @param   int $start
613     * @param   int $limit
614     * @return  array
615     */
616    public function retrieveGroups($start = 0, $limit = 0) {
617        /* Groups are always defined in the secondary auth plugin. */
618        $result = $this->authplugins['secondary']->retrieveGroups($start, $limit);
619        if (!$result) {
620            $this->_debug(
621                'authsplit:retrieveGroups(): secondary auth plugin\'s '.
622                'retrieveGroups() failed.', -1, __LINE__, __FILE__
623            );
624        }
625        return $result;
626    }
627
628    /**
629     * Return case sensitivity of the backend
630     *
631     * When your backend is caseinsensitive (eg. you can login with USER and
632     * user) then you need to overwrite this method and return false
633     *
634     * @return bool
635     */
636    public function isCaseSensitive() {
637        /* The primary auth plugin dictates case-sensitivity of login names. */
638        $result = $this->authplugins['primary']->isCaseSensitive();
639        $status = $result ? "Yes" : "No";
640        $this->_debug(
641            'authsplit:isCaseSensitive(): primary auth plugin\'s '.
642            'isCaseSensitive(): '.$status.'.', 1, __LINE__, __FILE__
643        );
644        return $result;
645    }
646
647    /**
648     * Sanitize a given username
649     *
650     * This function is applied to any user name that is given to
651     * the backend and should also be applied to any user name within
652     * the backend before returning it somewhere.
653     *
654     * This should be used to enforce username restrictions.
655     *
656     * @param string $user username
657     * @return string the cleaned username
658     */
659    public function cleanUser($user) {
660        /* The primary auth plugin dictates possible login name restrictions. */
661        $result = $this->authplugins['primary']->cleanUser($user);
662        $this->_debug(
663            'authsplit:cleanUser(): primary auth plugin\'s '.
664            'cleanUser("'.$user.'"): "'.$result.'".', 1, __LINE__, __FILE__
665        );
666
667        /* Apply case conversion? */
668        if ($this->username_caseconversion == 'To uppercase') {
669            $result = strtoupper($result);
670            $this->_debug(
671                'authsplit:cleanUser(): converted username to uppercase: '.
672                $result, 1, __LINE__, __FILE__
673            );
674        }
675        elseif ($this->username_caseconversion == 'To lowercase') {
676            $result = strtolower($result);
677            $this->_debug(
678                'authsplit:cleanUser(): converted username to lowercase: '.
679                $result, 1, __LINE__, __FILE__
680            );
681        }
682
683        return $result;
684    }
685
686    /**
687     * Sanitize a given groupname
688     *
689     * This function is applied to any groupname that is given to
690     * the backend and should also be applied to any groupname within
691     * the backend before returning it somewhere.
692     *
693     * This should be used to enforce groupname restrictions.
694     *
695     * Groupnames are to be passed without a leading '@' here.
696     *
697     * @param  string $group groupname
698     * @return string the cleaned groupname
699     */
700    public function cleanGroup($group) {
701        /* The secondary auth plugin dictates possible group names
702           restrictions. */
703        $result = $this->authplugins['secondary']->cleanGroup($group);
704        $this->_debug(
705            'authsplit:cleanGroup(): secondary auth plugin\'s '.
706            'cleanGroup("'.$group.'"): "'.$result.'".', 1, __LINE__, __FILE__
707        );
708        return $result;
709    }
710}
711
712// vim:ts=4:sw=4:et:
713