xref: /plugin/pureldap/classes/Client.php (revision 22654fdec35cb8c2bc6368625db77310d71208fb)
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    /**
115*22654fdeSAndreas Gohr     * Access to the config values
116*22654fdeSAndreas Gohr     *
117*22654fdeSAndreas Gohr     * Because the client class does configuration cleanup, the auth class should access
118*22654fdeSAndreas Gohr     * config values through the client
119*22654fdeSAndreas Gohr     *
120*22654fdeSAndreas Gohr     * @param string $key
121*22654fdeSAndreas Gohr     * @return mixed returns null on missing config
122*22654fdeSAndreas Gohr     */
123*22654fdeSAndreas Gohr    public function getConf($key) {
124*22654fdeSAndreas Gohr        if(!isset($this->config[$key])) return null;
125*22654fdeSAndreas Gohr        return $this->config[$key];
126*22654fdeSAndreas Gohr    }
127*22654fdeSAndreas Gohr
128*22654fdeSAndreas 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
1986d90d5c8SAndreas Gohr        $cachename = getCacheName($username, '.pureldap-user');
1996d90d5c8SAndreas Gohr        $cachetime = @filemtime($cachename);
2006d90d5c8SAndreas Gohr        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
2016d90d5c8SAndreas Gohr            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
2026d90d5c8SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
2036d90d5c8SAndreas Gohr                return $this->userCache[$username];
2046d90d5c8SAndreas Gohr            }
2056d90d5c8SAndreas Gohr        }
2066d90d5c8SAndreas Gohr
2071078ec26SAndreas Gohr        // fetch fresh data
2081078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
2091078ec26SAndreas Gohr
2101078ec26SAndreas Gohr        // store in cache
2111078ec26SAndreas Gohr        if ($info !== null) {
2121078ec26SAndreas Gohr            $this->userCache[$username] = $info;
2136d90d5c8SAndreas Gohr            file_put_contents($cachename, json_encode($info));
2141078ec26SAndreas Gohr        }
2151078ec26SAndreas Gohr
2161078ec26SAndreas Gohr        return $info;
2171078ec26SAndreas Gohr    }
2181078ec26SAndreas Gohr
2191078ec26SAndreas Gohr    /**
2201078ec26SAndreas Gohr     * Fetch a single user
2211078ec26SAndreas Gohr     *
2221078ec26SAndreas Gohr     * @param string $username
2231078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
2241078ec26SAndreas Gohr     * @return null|array
2251078ec26SAndreas Gohr     */
2261078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
2271078ec26SAndreas Gohr
2281078ec26SAndreas Gohr    /**
2295a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
2305a3b9122SAndreas Gohr     *
2315a3b9122SAndreas Gohr     * @return string[]
2325a3b9122SAndreas Gohr     */
2335a3b9122SAndreas Gohr    public function getCachedGroups()
2345a3b9122SAndreas Gohr    {
2355a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
2365a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
2375a3b9122SAndreas Gohr        }
2385a3b9122SAndreas Gohr
2395a3b9122SAndreas Gohr        return $this->groupCache;
2405a3b9122SAndreas Gohr    }
2415a3b9122SAndreas Gohr
2425a3b9122SAndreas Gohr    /**
2435a3b9122SAndreas Gohr     * Return a list of all available groups
2445a3b9122SAndreas Gohr     *
245b21740b4SAndreas Gohr     * Optionally filter the list
246b21740b4SAndreas Gohr     *
247b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
248b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2495a3b9122SAndreas Gohr     * @return string[]
2505a3b9122SAndreas Gohr     */
251204fba68SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
2525a3b9122SAndreas Gohr
25380ac552fSAndreas Gohr    /**
254a1128cc0SAndreas Gohr     * Clean the user name for use in DokuWiki
25580ac552fSAndreas Gohr     *
256a1128cc0SAndreas Gohr     * @param string $user
25780ac552fSAndreas Gohr     * @return string
25880ac552fSAndreas Gohr     */
259a1128cc0SAndreas Gohr    abstract public function cleanUser($user);
26080ac552fSAndreas Gohr
26180ac552fSAndreas Gohr    /**
262a1128cc0SAndreas Gohr     * Clean the group name for use in DokuWiki
26380ac552fSAndreas Gohr     *
264a1128cc0SAndreas Gohr     * @param string $group
26580ac552fSAndreas Gohr     * @return string
26680ac552fSAndreas Gohr     */
267a1128cc0SAndreas Gohr    abstract public function cleanGroup($group);
268a1128cc0SAndreas Gohr
269a1128cc0SAndreas Gohr    /**
270a1128cc0SAndreas Gohr     * Inheriting classes may want to manipulate the user before binding
271a1128cc0SAndreas Gohr     *
272a1128cc0SAndreas Gohr     * @param string $user
273a1128cc0SAndreas Gohr     * @return string
274a1128cc0SAndreas Gohr     */
275a1128cc0SAndreas Gohr    protected function prepareBindUser($user)
276a1128cc0SAndreas Gohr    {
277a1128cc0SAndreas Gohr        return $user;
278a1128cc0SAndreas Gohr    }
27980ac552fSAndreas Gohr
2805a3b9122SAndreas Gohr    /**
2811078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
2821078ec26SAndreas Gohr     *
2831078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
2841078ec26SAndreas Gohr     *
2851078ec26SAndreas Gohr     * @param Attribute|null $attribute
2861078ec26SAndreas Gohr     * @return string
2871078ec26SAndreas Gohr     */
2885a3b9122SAndreas Gohr    protected function attr2str($attribute)
2895a3b9122SAndreas Gohr    {
2901078ec26SAndreas Gohr        if ($attribute !== null) {
2911078ec26SAndreas Gohr            return $attribute->firstValue();
2921078ec26SAndreas Gohr        }
2931078ec26SAndreas Gohr        return '';
2941078ec26SAndreas Gohr    }
2951078ec26SAndreas Gohr
2961078ec26SAndreas Gohr    /**
2979c590892SAndreas Gohr     * Get the attributes that should be fetched for a user
2989c590892SAndreas Gohr     *
2999c590892SAndreas Gohr     * Can be extended in sub classes
3009c590892SAndreas Gohr     *
3019c590892SAndreas Gohr     * @return Attribute[]
3029c590892SAndreas Gohr     */
3039c590892SAndreas Gohr    protected function userAttributes()
3049c590892SAndreas Gohr    {
3059c590892SAndreas Gohr        // defaults
3069c590892SAndreas Gohr        $attr = [
3079c590892SAndreas Gohr            new Attribute('dn'),
3089c590892SAndreas Gohr            new Attribute('displayName'),
3099c590892SAndreas Gohr            new Attribute('mail'),
3109c590892SAndreas Gohr        ];
3119c590892SAndreas Gohr        // additionals
3129c590892SAndreas Gohr        foreach ($this->config['attributes'] as $attribute) {
3139c590892SAndreas Gohr            $attr[] = new Attribute($attribute);
3149c590892SAndreas Gohr        }
3159c590892SAndreas Gohr        return $attr;
3169c590892SAndreas Gohr    }
3179c590892SAndreas Gohr
3189c590892SAndreas Gohr    /**
319da369b60SAndreas Gohr     * Handle fatal exceptions
3201078ec26SAndreas Gohr     *
3211078ec26SAndreas Gohr     * @param \Exception $e
3221078ec26SAndreas Gohr     */
323da369b60SAndreas Gohr    protected function fatal(\Exception $e)
3241078ec26SAndreas Gohr    {
325c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\ErrorHandler')) {
326c872f0e3SAndreas Gohr            \dokuwiki\ErrorHandler::logException($e);
327c872f0e3SAndreas Gohr        }
328c872f0e3SAndreas Gohr
3291078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
3308595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
3311078ec26SAndreas Gohr        }
332c872f0e3SAndreas Gohr
333c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()), -1);
334da369b60SAndreas Gohr    }
3351078ec26SAndreas Gohr
336da369b60SAndreas Gohr    /**
337a1128cc0SAndreas Gohr     * Handle error output
338a1128cc0SAndreas Gohr     *
339a1128cc0SAndreas Gohr     * @param string $msg
340a1128cc0SAndreas Gohr     * @param string $file
341a1128cc0SAndreas Gohr     * @param int $line
342a1128cc0SAndreas Gohr     */
343a1128cc0SAndreas Gohr    protected function error($msg, $file, $line)
344a1128cc0SAndreas Gohr    {
345c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
346c872f0e3SAndreas Gohr            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
347c872f0e3SAndreas Gohr        }
348c872f0e3SAndreas Gohr
349a1128cc0SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
350a1128cc0SAndreas Gohr            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
351a1128cc0SAndreas Gohr        }
352a1128cc0SAndreas Gohr
353c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($msg), -1);
354a1128cc0SAndreas Gohr    }
355a1128cc0SAndreas Gohr
356a1128cc0SAndreas Gohr    /**
357da369b60SAndreas Gohr     * Handle debug output
358da369b60SAndreas Gohr     *
359da369b60SAndreas Gohr     * @param string $msg
360da369b60SAndreas Gohr     * @param string $file
361da369b60SAndreas Gohr     * @param int $line
362da369b60SAndreas Gohr     */
363da369b60SAndreas Gohr    protected function debug($msg, $file, $line)
364da369b60SAndreas Gohr    {
365c872f0e3SAndreas Gohr        global $conf;
366c872f0e3SAndreas Gohr
367c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
368c872f0e3SAndreas Gohr            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
369c872f0e3SAndreas Gohr        }
370c872f0e3SAndreas Gohr
371c872f0e3SAndreas Gohr        if ($conf['allowdebug']) {
37285916a2dSAndreas Gohr            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
3731078ec26SAndreas Gohr        }
3741078ec26SAndreas Gohr    }
375c872f0e3SAndreas Gohr}
376