xref: /plugin/pureldap/classes/Client.php (revision 5dcabeda2fad4e4ee9d5e2783f1e5e830b0344f4)
11078ec26SAndreas Gohr<?php
21078ec26SAndreas Gohr
31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes;
41078ec26SAndreas Gohr
580ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString;
61078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute;
71078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\BindException;
81078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\ConnectionException;
91078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException;
101078ec26SAndreas Gohruse FreeDSx\Ldap\LdapClient;
111078ec26SAndreas Gohr
121078ec26SAndreas Gohrrequire_once __DIR__ . '/../vendor/autoload.php';
131078ec26SAndreas Gohr
141078ec26SAndreas Gohrabstract class Client
151078ec26SAndreas Gohr{
16204fba68SAndreas Gohr    const FILTER_EQUAL = 'equal';
17204fba68SAndreas Gohr    const FILTER_CONTAINS = 'contains';
18204fba68SAndreas Gohr    const FILTER_STARTSWITH = 'startsWith';
19204fba68SAndreas Gohr    const FILTER_ENDSWITH = 'endsWith';
20204fba68SAndreas Gohr
211078ec26SAndreas Gohr    /** @var array the configuration */
221078ec26SAndreas Gohr    protected $config;
231078ec26SAndreas Gohr
241078ec26SAndreas Gohr    /** @var LdapClient */
251078ec26SAndreas Gohr    protected $ldap;
261078ec26SAndreas Gohr
271078ec26SAndreas Gohr    /** @var bool is this client authenticated already? */
281078ec26SAndreas Gohr    protected $isAuthenticated = false;
291078ec26SAndreas Gohr
301078ec26SAndreas Gohr    /** @var array cached user info */
311078ec26SAndreas Gohr    protected $userCache = [];
321078ec26SAndreas Gohr
335a3b9122SAndreas Gohr    /** @var array cached group list */
345a3b9122SAndreas Gohr    protected $groupCache = [];
355a3b9122SAndreas Gohr
361078ec26SAndreas Gohr    /**
371078ec26SAndreas Gohr     * Client constructor.
381078ec26SAndreas Gohr     * @param array $config
391078ec26SAndreas Gohr     */
401078ec26SAndreas Gohr    public function __construct($config)
411078ec26SAndreas Gohr    {
421078ec26SAndreas Gohr        $this->config = $this->prepareConfig($config);
43bf69b89cSAndreas Gohr        $this->prepareSSO();
441078ec26SAndreas Gohr        $this->ldap = new LdapClient($this->config);
451078ec26SAndreas Gohr    }
461078ec26SAndreas Gohr
471078ec26SAndreas Gohr    /**
481078ec26SAndreas Gohr     * Setup sane config defaults
491078ec26SAndreas Gohr     *
501078ec26SAndreas Gohr     * @param array $config
511078ec26SAndreas Gohr     * @return array
521078ec26SAndreas Gohr     */
531078ec26SAndreas Gohr    protected function prepareConfig($config)
541078ec26SAndreas Gohr    {
551a4f0e1fSAndreas Gohr        // ensure we have the default keys
561a4f0e1fSAndreas Gohr        /** @var array $conf */
571a4f0e1fSAndreas Gohr        include __DIR__ . '/../conf/default.php';
581a4f0e1fSAndreas Gohr        $defaults = $conf;
591a4f0e1fSAndreas Gohr        $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf
601078ec26SAndreas Gohr
611078ec26SAndreas Gohr        $config = array_merge($defaults, $config);
621078ec26SAndreas Gohr
631078ec26SAndreas Gohr        // default port depends on SSL setting
641078ec26SAndreas Gohr        if (!$config['port']) {
656d90d5c8SAndreas Gohr            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
666d90d5c8SAndreas Gohr        }
676d90d5c8SAndreas Gohr
686d90d5c8SAndreas Gohr        // set ssl parameters
696d90d5c8SAndreas Gohr        $config['use_ssl'] = ($config['encryption'] === 'ssl');
706d90d5c8SAndreas Gohr        if ($config['validate'] === 'none') {
716d90d5c8SAndreas Gohr            $config['ssl_validate_cert'] = false;
726d90d5c8SAndreas Gohr        } elseif ($config['validate'] === 'self') {
736d90d5c8SAndreas Gohr            $config['ssl_allow_self_signed'] = true;
741078ec26SAndreas Gohr        }
751078ec26SAndreas Gohr
76a1128cc0SAndreas Gohr        $config['suffix'] = PhpString::strtolower($config['suffix']);
77c2500b44SAndreas Gohr        $config['primarygroup'] = $this->cleanGroup($config['primarygroup']);
7880ac552fSAndreas Gohr
791078ec26SAndreas Gohr        return $config;
801078ec26SAndreas Gohr    }
811078ec26SAndreas Gohr
821078ec26SAndreas Gohr    /**
83bf69b89cSAndreas Gohr     * Extract user info from environment for SSO
84bf69b89cSAndreas Gohr     */
85bf69b89cSAndreas Gohr    protected function prepareSSO()
86bf69b89cSAndreas Gohr    {
87bf69b89cSAndreas Gohr        global $INPUT;
88bf69b89cSAndreas Gohr
89bf69b89cSAndreas Gohr        if (!$this->config['sso']) return;
90bf69b89cSAndreas Gohr        if ($INPUT->server->str('REMOTE_USER') === '') return;
91bf69b89cSAndreas Gohr
92bf69b89cSAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
93bf69b89cSAndreas Gohr
94bf69b89cSAndreas Gohr        // make sure the right encoding is used
95bf69b89cSAndreas Gohr        if ($this->config['sso_charset']) {
96bf69b89cSAndreas Gohr            $user = iconv($this->config['sso_charset'], 'UTF-8', $user);
97bf69b89cSAndreas Gohr        } elseif (!\dokuwiki\Utf8\Clean::isUtf8($user)) {
98bf69b89cSAndreas Gohr            $user = utf8_encode($user);
99bf69b89cSAndreas Gohr        }
100bf69b89cSAndreas Gohr        $user = $this->cleanUser($user);
101bf69b89cSAndreas Gohr
102bf69b89cSAndreas Gohr        // Prepare SSO
103bf69b89cSAndreas Gohr
104bf69b89cSAndreas Gohr        // trust the incoming user
105bf69b89cSAndreas Gohr        $INPUT->server->set('REMOTE_USER', $user);
106bf69b89cSAndreas Gohr
107bf69b89cSAndreas Gohr        // we need to simulate a login
108bf69b89cSAndreas Gohr        if (empty($_COOKIE[DOKU_COOKIE])) {
109bf69b89cSAndreas Gohr            $INPUT->set('u', $user);
110bf69b89cSAndreas Gohr            $INPUT->set('p', 'sso_only');
111bf69b89cSAndreas Gohr        }
112bf69b89cSAndreas Gohr    }
113bf69b89cSAndreas Gohr
114bf69b89cSAndreas Gohr    /**
11522654fdeSAndreas Gohr     * Access to the config values
11622654fdeSAndreas Gohr     *
11722654fdeSAndreas Gohr     * Because the client class does configuration cleanup, the auth class should access
11822654fdeSAndreas Gohr     * config values through the client
11922654fdeSAndreas Gohr     *
12022654fdeSAndreas Gohr     * @param string $key
12122654fdeSAndreas Gohr     * @return mixed returns null on missing config
12222654fdeSAndreas Gohr     */
12322654fdeSAndreas Gohr    public function getConf($key) {
12422654fdeSAndreas Gohr        if(!isset($this->config[$key])) return null;
12522654fdeSAndreas Gohr        return $this->config[$key];
12622654fdeSAndreas Gohr    }
12722654fdeSAndreas Gohr
12822654fdeSAndreas Gohr    /**
1291078ec26SAndreas Gohr     * Authenticate as admin
1301078ec26SAndreas Gohr     */
1311078ec26SAndreas Gohr    public function autoAuth()
1321078ec26SAndreas Gohr    {
1331078ec26SAndreas Gohr        if ($this->isAuthenticated) return true;
1349446f9efSAndreas Gohr
135a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($this->config['admin_username']);
1369446f9efSAndreas Gohr        $ok = $this->authenticate($user, $this->config['admin_password']);
1378b2677edSAndreas Gohr        if (!$ok) {
138a1128cc0SAndreas Gohr            $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
1398b2677edSAndreas Gohr        }
1408b2677edSAndreas Gohr        return $ok;
1411078ec26SAndreas Gohr    }
1421078ec26SAndreas Gohr
1431078ec26SAndreas Gohr    /**
1441078ec26SAndreas Gohr     * Authenticates a given user. This client will remain authenticated
1451078ec26SAndreas Gohr     *
1461078ec26SAndreas Gohr     * @param string $user
1471078ec26SAndreas Gohr     * @param string $pass
1481078ec26SAndreas Gohr     * @return bool was the authentication successful?
1495a3b9122SAndreas Gohr     * @noinspection PhpRedundantCatchClauseInspection
1501078ec26SAndreas Gohr     */
1511078ec26SAndreas Gohr    public function authenticate($user, $pass)
1521078ec26SAndreas Gohr    {
153a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($user);
15480ac552fSAndreas Gohr
1556d90d5c8SAndreas Gohr        if ($this->config['encryption'] === 'tls') {
1561078ec26SAndreas Gohr            try {
1571078ec26SAndreas Gohr                $this->ldap->startTls();
1581078ec26SAndreas Gohr            } catch (OperationException $e) {
159da369b60SAndreas Gohr                $this->fatal($e);
1601078ec26SAndreas Gohr            }
1611078ec26SAndreas Gohr        }
1621078ec26SAndreas Gohr
1631078ec26SAndreas Gohr        try {
1641078ec26SAndreas Gohr            $this->ldap->bind($user, $pass);
1651078ec26SAndreas Gohr        } catch (BindException $e) {
1661078ec26SAndreas Gohr            return false;
1671078ec26SAndreas Gohr        } catch (ConnectionException $e) {
168da369b60SAndreas Gohr            $this->fatal($e);
1691078ec26SAndreas Gohr            return false;
1701078ec26SAndreas Gohr        } catch (OperationException $e) {
171da369b60SAndreas Gohr            $this->fatal($e);
1721078ec26SAndreas Gohr            return false;
1731078ec26SAndreas Gohr        }
1741078ec26SAndreas Gohr
1751078ec26SAndreas Gohr        $this->isAuthenticated = true;
1761078ec26SAndreas Gohr        return true;
1771078ec26SAndreas Gohr    }
1781078ec26SAndreas Gohr
1791078ec26SAndreas Gohr    /**
1801078ec26SAndreas Gohr     * Get info for a single user, use cache if available
1811078ec26SAndreas Gohr     *
1821078ec26SAndreas Gohr     * @param string $username
1831078ec26SAndreas Gohr     * @param bool $fetchgroups Are groups needed?
1841078ec26SAndreas Gohr     * @return array|null
1851078ec26SAndreas Gohr     */
1861078ec26SAndreas Gohr    public function getCachedUser($username, $fetchgroups = true)
1871078ec26SAndreas Gohr    {
1886d90d5c8SAndreas Gohr        global $conf;
1896d90d5c8SAndreas Gohr
1906d90d5c8SAndreas Gohr        // memory cache first
1911078ec26SAndreas Gohr        if (isset($this->userCache[$username])) {
1921078ec26SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1931078ec26SAndreas Gohr                return $this->userCache[$username];
1941078ec26SAndreas Gohr            }
1951078ec26SAndreas Gohr        }
1961078ec26SAndreas Gohr
1976d90d5c8SAndreas Gohr        // disk cache second
198*5dcabedaSAndreas Gohr        if($this->config['usefscache']) {
1996d90d5c8SAndreas Gohr            $cachename = getCacheName($username, '.pureldap-user');
2006d90d5c8SAndreas Gohr            $cachetime = @filemtime($cachename);
2016d90d5c8SAndreas Gohr            if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
2026d90d5c8SAndreas Gohr                $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
2036d90d5c8SAndreas Gohr                if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
2046d90d5c8SAndreas Gohr                    return $this->userCache[$username];
2056d90d5c8SAndreas Gohr                }
2066d90d5c8SAndreas Gohr            }
207*5dcabedaSAndreas Gohr        }
2086d90d5c8SAndreas Gohr
2091078ec26SAndreas Gohr        // fetch fresh data
2101078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
2111078ec26SAndreas Gohr
2121078ec26SAndreas Gohr        // store in cache
213*5dcabedaSAndreas Gohr        if ($this->config['usefscache'] && $info !== null) {
2141078ec26SAndreas Gohr            $this->userCache[$username] = $info;
2156d90d5c8SAndreas Gohr            file_put_contents($cachename, json_encode($info));
2161078ec26SAndreas Gohr        }
2171078ec26SAndreas Gohr
2181078ec26SAndreas Gohr        return $info;
2191078ec26SAndreas Gohr    }
2201078ec26SAndreas Gohr
2211078ec26SAndreas Gohr    /**
2221078ec26SAndreas Gohr     * Fetch a single user
2231078ec26SAndreas Gohr     *
2241078ec26SAndreas Gohr     * @param string $username
2251078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
2261078ec26SAndreas Gohr     * @return null|array
2271078ec26SAndreas Gohr     */
2281078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
2291078ec26SAndreas Gohr
2301078ec26SAndreas Gohr    /**
2315a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
2325a3b9122SAndreas Gohr     *
2335a3b9122SAndreas Gohr     * @return string[]
2345a3b9122SAndreas Gohr     */
2355a3b9122SAndreas Gohr    public function getCachedGroups()
2365a3b9122SAndreas Gohr    {
2375a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
2385a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
2395a3b9122SAndreas Gohr        }
2405a3b9122SAndreas Gohr
2415a3b9122SAndreas Gohr        return $this->groupCache;
2425a3b9122SAndreas Gohr    }
2435a3b9122SAndreas Gohr
2445a3b9122SAndreas Gohr    /**
2455a3b9122SAndreas Gohr     * Return a list of all available groups
2465a3b9122SAndreas Gohr     *
247b21740b4SAndreas Gohr     * Optionally filter the list
248b21740b4SAndreas Gohr     *
249b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
250b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2515a3b9122SAndreas Gohr     * @return string[]
2525a3b9122SAndreas Gohr     */
253204fba68SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
2545a3b9122SAndreas Gohr
25580ac552fSAndreas Gohr    /**
256a1128cc0SAndreas Gohr     * Clean the user name for use in DokuWiki
25780ac552fSAndreas Gohr     *
258a1128cc0SAndreas Gohr     * @param string $user
25980ac552fSAndreas Gohr     * @return string
26080ac552fSAndreas Gohr     */
261a1128cc0SAndreas Gohr    abstract public function cleanUser($user);
26280ac552fSAndreas Gohr
26380ac552fSAndreas Gohr    /**
264a1128cc0SAndreas Gohr     * Clean the group name for use in DokuWiki
26580ac552fSAndreas Gohr     *
266a1128cc0SAndreas Gohr     * @param string $group
26780ac552fSAndreas Gohr     * @return string
26880ac552fSAndreas Gohr     */
269a1128cc0SAndreas Gohr    abstract public function cleanGroup($group);
270a1128cc0SAndreas Gohr
271a1128cc0SAndreas Gohr    /**
272a1128cc0SAndreas Gohr     * Inheriting classes may want to manipulate the user before binding
273a1128cc0SAndreas Gohr     *
274a1128cc0SAndreas Gohr     * @param string $user
275a1128cc0SAndreas Gohr     * @return string
276a1128cc0SAndreas Gohr     */
277a1128cc0SAndreas Gohr    protected function prepareBindUser($user)
278a1128cc0SAndreas Gohr    {
279a1128cc0SAndreas Gohr        return $user;
280a1128cc0SAndreas Gohr    }
28180ac552fSAndreas Gohr
2825a3b9122SAndreas Gohr    /**
2831078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
2841078ec26SAndreas Gohr     *
2851078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
2861078ec26SAndreas Gohr     *
2871078ec26SAndreas Gohr     * @param Attribute|null $attribute
2881078ec26SAndreas Gohr     * @return string
2891078ec26SAndreas Gohr     */
2905a3b9122SAndreas Gohr    protected function attr2str($attribute)
2915a3b9122SAndreas Gohr    {
2921078ec26SAndreas Gohr        if ($attribute !== null) {
2931078ec26SAndreas Gohr            return $attribute->firstValue();
2941078ec26SAndreas Gohr        }
2951078ec26SAndreas Gohr        return '';
2961078ec26SAndreas Gohr    }
2971078ec26SAndreas Gohr
2981078ec26SAndreas Gohr    /**
2999c590892SAndreas Gohr     * Get the attributes that should be fetched for a user
3009c590892SAndreas Gohr     *
3019c590892SAndreas Gohr     * Can be extended in sub classes
3029c590892SAndreas Gohr     *
3039c590892SAndreas Gohr     * @return Attribute[]
3049c590892SAndreas Gohr     */
3059c590892SAndreas Gohr    protected function userAttributes()
3069c590892SAndreas Gohr    {
3079c590892SAndreas Gohr        // defaults
3089c590892SAndreas Gohr        $attr = [
3099c590892SAndreas Gohr            new Attribute('dn'),
3109c590892SAndreas Gohr            new Attribute('displayName'),
3119c590892SAndreas Gohr            new Attribute('mail'),
3129c590892SAndreas Gohr        ];
3139c590892SAndreas Gohr        // additionals
3149c590892SAndreas Gohr        foreach ($this->config['attributes'] as $attribute) {
3159c590892SAndreas Gohr            $attr[] = new Attribute($attribute);
3169c590892SAndreas Gohr        }
3179c590892SAndreas Gohr        return $attr;
3189c590892SAndreas Gohr    }
3199c590892SAndreas Gohr
3209c590892SAndreas Gohr    /**
321da369b60SAndreas Gohr     * Handle fatal exceptions
3221078ec26SAndreas Gohr     *
3231078ec26SAndreas Gohr     * @param \Exception $e
3241078ec26SAndreas Gohr     */
325da369b60SAndreas Gohr    protected function fatal(\Exception $e)
3261078ec26SAndreas Gohr    {
327c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\ErrorHandler')) {
328c872f0e3SAndreas Gohr            \dokuwiki\ErrorHandler::logException($e);
329c872f0e3SAndreas Gohr        }
330c872f0e3SAndreas Gohr
3311078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
3328595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
3331078ec26SAndreas Gohr        }
334c872f0e3SAndreas Gohr
335c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()), -1);
336da369b60SAndreas Gohr    }
3371078ec26SAndreas Gohr
338da369b60SAndreas Gohr    /**
339a1128cc0SAndreas Gohr     * Handle error output
340a1128cc0SAndreas Gohr     *
341a1128cc0SAndreas Gohr     * @param string $msg
342a1128cc0SAndreas Gohr     * @param string $file
343a1128cc0SAndreas Gohr     * @param int $line
344a1128cc0SAndreas Gohr     */
345a1128cc0SAndreas Gohr    protected function error($msg, $file, $line)
346a1128cc0SAndreas Gohr    {
347c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
348c872f0e3SAndreas Gohr            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
349c872f0e3SAndreas Gohr        }
350c872f0e3SAndreas Gohr
351a1128cc0SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
352a1128cc0SAndreas Gohr            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
353a1128cc0SAndreas Gohr        }
354a1128cc0SAndreas Gohr
355c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($msg), -1);
356a1128cc0SAndreas Gohr    }
357a1128cc0SAndreas Gohr
358a1128cc0SAndreas Gohr    /**
359da369b60SAndreas Gohr     * Handle debug output
360da369b60SAndreas Gohr     *
361da369b60SAndreas Gohr     * @param string $msg
362da369b60SAndreas Gohr     * @param string $file
363da369b60SAndreas Gohr     * @param int $line
364da369b60SAndreas Gohr     */
365da369b60SAndreas Gohr    protected function debug($msg, $file, $line)
366da369b60SAndreas Gohr    {
367c872f0e3SAndreas Gohr        global $conf;
368c872f0e3SAndreas Gohr
369c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
370c872f0e3SAndreas Gohr            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
371c872f0e3SAndreas Gohr        }
372c872f0e3SAndreas Gohr
373c872f0e3SAndreas Gohr        if ($conf['allowdebug']) {
37485916a2dSAndreas Gohr            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
3751078ec26SAndreas Gohr        }
3761078ec26SAndreas Gohr    }
377c872f0e3SAndreas Gohr}
378