xref: /plugin/pureldap/classes/Client.php (revision bf69b89c042d8a8a10fdd1dd78cc14b8f933bfe1)
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);
43*bf69b89cSAndreas 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    /**
83*bf69b89cSAndreas Gohr     * Extract user info from environment for SSO
84*bf69b89cSAndreas Gohr     */
85*bf69b89cSAndreas Gohr    protected function prepareSSO()
86*bf69b89cSAndreas Gohr    {
87*bf69b89cSAndreas Gohr        global $INPUT;
88*bf69b89cSAndreas Gohr
89*bf69b89cSAndreas Gohr        if (!$this->config['sso']) return;
90*bf69b89cSAndreas Gohr        if ($INPUT->server->str('REMOTE_USER') === '') return;
91*bf69b89cSAndreas Gohr
92*bf69b89cSAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
93*bf69b89cSAndreas Gohr
94*bf69b89cSAndreas Gohr        // make sure the right encoding is used
95*bf69b89cSAndreas Gohr        if ($this->config['sso_charset']) {
96*bf69b89cSAndreas Gohr            $user = iconv($this->config['sso_charset'], 'UTF-8', $user);
97*bf69b89cSAndreas Gohr        } elseif (!\dokuwiki\Utf8\Clean::isUtf8($user)) {
98*bf69b89cSAndreas Gohr            $user = utf8_encode($user);
99*bf69b89cSAndreas Gohr        }
100*bf69b89cSAndreas Gohr        $user = $this->cleanUser($user);
101*bf69b89cSAndreas Gohr
102*bf69b89cSAndreas Gohr        // Prepare SSO
103*bf69b89cSAndreas Gohr
104*bf69b89cSAndreas Gohr        // trust the incoming user
105*bf69b89cSAndreas Gohr        $INPUT->server->set('REMOTE_USER', $user);
106*bf69b89cSAndreas Gohr
107*bf69b89cSAndreas Gohr        // we need to simulate a login
108*bf69b89cSAndreas Gohr        if (empty($_COOKIE[DOKU_COOKIE])) {
109*bf69b89cSAndreas Gohr            $INPUT->set('u', $user);
110*bf69b89cSAndreas Gohr            $INPUT->set('p', 'sso_only');
111*bf69b89cSAndreas Gohr        }
112*bf69b89cSAndreas Gohr    }
113*bf69b89cSAndreas Gohr
114*bf69b89cSAndreas Gohr    /**
1151078ec26SAndreas Gohr     * Authenticate as admin
1161078ec26SAndreas Gohr     */
1171078ec26SAndreas Gohr    public function autoAuth()
1181078ec26SAndreas Gohr    {
1191078ec26SAndreas Gohr        if ($this->isAuthenticated) return true;
1209446f9efSAndreas Gohr
121a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($this->config['admin_username']);
1229446f9efSAndreas Gohr        $ok = $this->authenticate($user, $this->config['admin_password']);
1238b2677edSAndreas Gohr        if (!$ok) {
124a1128cc0SAndreas Gohr            $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
1258b2677edSAndreas Gohr        }
1268b2677edSAndreas Gohr        return $ok;
1271078ec26SAndreas Gohr    }
1281078ec26SAndreas Gohr
1291078ec26SAndreas Gohr    /**
1301078ec26SAndreas Gohr     * Authenticates a given user. This client will remain authenticated
1311078ec26SAndreas Gohr     *
1321078ec26SAndreas Gohr     * @param string $user
1331078ec26SAndreas Gohr     * @param string $pass
1341078ec26SAndreas Gohr     * @return bool was the authentication successful?
1355a3b9122SAndreas Gohr     * @noinspection PhpRedundantCatchClauseInspection
1361078ec26SAndreas Gohr     */
1371078ec26SAndreas Gohr    public function authenticate($user, $pass)
1381078ec26SAndreas Gohr    {
139a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($user);
14080ac552fSAndreas Gohr
1416d90d5c8SAndreas Gohr        if ($this->config['encryption'] === 'tls') {
1421078ec26SAndreas Gohr            try {
1431078ec26SAndreas Gohr                $this->ldap->startTls();
1441078ec26SAndreas Gohr            } catch (OperationException $e) {
145da369b60SAndreas Gohr                $this->fatal($e);
1461078ec26SAndreas Gohr            }
1471078ec26SAndreas Gohr        }
1481078ec26SAndreas Gohr
1491078ec26SAndreas Gohr        try {
1501078ec26SAndreas Gohr            $this->ldap->bind($user, $pass);
1511078ec26SAndreas Gohr        } catch (BindException $e) {
1521078ec26SAndreas Gohr            return false;
1531078ec26SAndreas Gohr        } catch (ConnectionException $e) {
154da369b60SAndreas Gohr            $this->fatal($e);
1551078ec26SAndreas Gohr            return false;
1561078ec26SAndreas Gohr        } catch (OperationException $e) {
157da369b60SAndreas Gohr            $this->fatal($e);
1581078ec26SAndreas Gohr            return false;
1591078ec26SAndreas Gohr        }
1601078ec26SAndreas Gohr
1611078ec26SAndreas Gohr        $this->isAuthenticated = true;
1621078ec26SAndreas Gohr        return true;
1631078ec26SAndreas Gohr    }
1641078ec26SAndreas Gohr
1651078ec26SAndreas Gohr    /**
1661078ec26SAndreas Gohr     * Get info for a single user, use cache if available
1671078ec26SAndreas Gohr     *
1681078ec26SAndreas Gohr     * @param string $username
1691078ec26SAndreas Gohr     * @param bool $fetchgroups Are groups needed?
1701078ec26SAndreas Gohr     * @return array|null
1711078ec26SAndreas Gohr     */
1721078ec26SAndreas Gohr    public function getCachedUser($username, $fetchgroups = true)
1731078ec26SAndreas Gohr    {
1746d90d5c8SAndreas Gohr        global $conf;
1756d90d5c8SAndreas Gohr
1766d90d5c8SAndreas Gohr        // memory cache first
1771078ec26SAndreas Gohr        if (isset($this->userCache[$username])) {
1781078ec26SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1791078ec26SAndreas Gohr                return $this->userCache[$username];
1801078ec26SAndreas Gohr            }
1811078ec26SAndreas Gohr        }
1821078ec26SAndreas Gohr
1836d90d5c8SAndreas Gohr        // disk cache second
1846d90d5c8SAndreas Gohr        $cachename = getCacheName($username, '.pureldap-user');
1856d90d5c8SAndreas Gohr        $cachetime = @filemtime($cachename);
1866d90d5c8SAndreas Gohr        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
1876d90d5c8SAndreas Gohr            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
1886d90d5c8SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1896d90d5c8SAndreas Gohr                return $this->userCache[$username];
1906d90d5c8SAndreas Gohr            }
1916d90d5c8SAndreas Gohr        }
1926d90d5c8SAndreas Gohr
1931078ec26SAndreas Gohr        // fetch fresh data
1941078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
1951078ec26SAndreas Gohr
1961078ec26SAndreas Gohr        // store in cache
1971078ec26SAndreas Gohr        if ($info !== null) {
1981078ec26SAndreas Gohr            $this->userCache[$username] = $info;
1996d90d5c8SAndreas Gohr            file_put_contents($cachename, json_encode($info));
2001078ec26SAndreas Gohr        }
2011078ec26SAndreas Gohr
2021078ec26SAndreas Gohr        return $info;
2031078ec26SAndreas Gohr    }
2041078ec26SAndreas Gohr
2051078ec26SAndreas Gohr    /**
2061078ec26SAndreas Gohr     * Fetch a single user
2071078ec26SAndreas Gohr     *
2081078ec26SAndreas Gohr     * @param string $username
2091078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
2101078ec26SAndreas Gohr     * @return null|array
2111078ec26SAndreas Gohr     */
2121078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
2131078ec26SAndreas Gohr
2141078ec26SAndreas Gohr    /**
2155a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
2165a3b9122SAndreas Gohr     *
2175a3b9122SAndreas Gohr     * @return string[]
2185a3b9122SAndreas Gohr     */
2195a3b9122SAndreas Gohr    public function getCachedGroups()
2205a3b9122SAndreas Gohr    {
2215a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
2225a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
2235a3b9122SAndreas Gohr        }
2245a3b9122SAndreas Gohr
2255a3b9122SAndreas Gohr        return $this->groupCache;
2265a3b9122SAndreas Gohr    }
2275a3b9122SAndreas Gohr
2285a3b9122SAndreas Gohr    /**
2295a3b9122SAndreas Gohr     * Return a list of all available groups
2305a3b9122SAndreas Gohr     *
231b21740b4SAndreas Gohr     * Optionally filter the list
232b21740b4SAndreas Gohr     *
233b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
234b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2355a3b9122SAndreas Gohr     * @return string[]
2365a3b9122SAndreas Gohr     */
237204fba68SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
2385a3b9122SAndreas Gohr
23980ac552fSAndreas Gohr    /**
240a1128cc0SAndreas Gohr     * Clean the user name for use in DokuWiki
24180ac552fSAndreas Gohr     *
242a1128cc0SAndreas Gohr     * @param string $user
24380ac552fSAndreas Gohr     * @return string
24480ac552fSAndreas Gohr     */
245a1128cc0SAndreas Gohr    abstract public function cleanUser($user);
24680ac552fSAndreas Gohr
24780ac552fSAndreas Gohr    /**
248a1128cc0SAndreas Gohr     * Clean the group name for use in DokuWiki
24980ac552fSAndreas Gohr     *
250a1128cc0SAndreas Gohr     * @param string $group
25180ac552fSAndreas Gohr     * @return string
25280ac552fSAndreas Gohr     */
253a1128cc0SAndreas Gohr    abstract public function cleanGroup($group);
254a1128cc0SAndreas Gohr
255a1128cc0SAndreas Gohr    /**
256a1128cc0SAndreas Gohr     * Inheriting classes may want to manipulate the user before binding
257a1128cc0SAndreas Gohr     *
258a1128cc0SAndreas Gohr     * @param string $user
259a1128cc0SAndreas Gohr     * @return string
260a1128cc0SAndreas Gohr     */
261a1128cc0SAndreas Gohr    protected function prepareBindUser($user)
262a1128cc0SAndreas Gohr    {
263a1128cc0SAndreas Gohr        return $user;
264a1128cc0SAndreas Gohr    }
26580ac552fSAndreas Gohr
2665a3b9122SAndreas Gohr    /**
2671078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
2681078ec26SAndreas Gohr     *
2691078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
2701078ec26SAndreas Gohr     *
2711078ec26SAndreas Gohr     * @param Attribute|null $attribute
2721078ec26SAndreas Gohr     * @return string
2731078ec26SAndreas Gohr     */
2745a3b9122SAndreas Gohr    protected function attr2str($attribute)
2755a3b9122SAndreas Gohr    {
2761078ec26SAndreas Gohr        if ($attribute !== null) {
2771078ec26SAndreas Gohr            return $attribute->firstValue();
2781078ec26SAndreas Gohr        }
2791078ec26SAndreas Gohr        return '';
2801078ec26SAndreas Gohr    }
2811078ec26SAndreas Gohr
2821078ec26SAndreas Gohr    /**
2839c590892SAndreas Gohr     * Get the attributes that should be fetched for a user
2849c590892SAndreas Gohr     *
2859c590892SAndreas Gohr     * Can be extended in sub classes
2869c590892SAndreas Gohr     *
2879c590892SAndreas Gohr     * @return Attribute[]
2889c590892SAndreas Gohr     */
2899c590892SAndreas Gohr    protected function userAttributes()
2909c590892SAndreas Gohr    {
2919c590892SAndreas Gohr        // defaults
2929c590892SAndreas Gohr        $attr = [
2939c590892SAndreas Gohr            new Attribute('dn'),
2949c590892SAndreas Gohr            new Attribute('displayName'),
2959c590892SAndreas Gohr            new Attribute('mail'),
2969c590892SAndreas Gohr        ];
2979c590892SAndreas Gohr        // additionals
2989c590892SAndreas Gohr        foreach ($this->config['attributes'] as $attribute) {
2999c590892SAndreas Gohr            $attr[] = new Attribute($attribute);
3009c590892SAndreas Gohr        }
3019c590892SAndreas Gohr        return $attr;
3029c590892SAndreas Gohr    }
3039c590892SAndreas Gohr
3049c590892SAndreas Gohr    /**
305da369b60SAndreas Gohr     * Handle fatal exceptions
3061078ec26SAndreas Gohr     *
3071078ec26SAndreas Gohr     * @param \Exception $e
3081078ec26SAndreas Gohr     */
309da369b60SAndreas Gohr    protected function fatal(\Exception $e)
3101078ec26SAndreas Gohr    {
311c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\ErrorHandler')) {
312c872f0e3SAndreas Gohr            \dokuwiki\ErrorHandler::logException($e);
313c872f0e3SAndreas Gohr        }
314c872f0e3SAndreas Gohr
3151078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
3168595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
3171078ec26SAndreas Gohr        }
318c872f0e3SAndreas Gohr
319c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()), -1);
320da369b60SAndreas Gohr    }
3211078ec26SAndreas Gohr
322da369b60SAndreas Gohr    /**
323a1128cc0SAndreas Gohr     * Handle error output
324a1128cc0SAndreas Gohr     *
325a1128cc0SAndreas Gohr     * @param string $msg
326a1128cc0SAndreas Gohr     * @param string $file
327a1128cc0SAndreas Gohr     * @param int $line
328a1128cc0SAndreas Gohr     */
329a1128cc0SAndreas Gohr    protected function error($msg, $file, $line)
330a1128cc0SAndreas Gohr    {
331c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
332c872f0e3SAndreas Gohr            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
333c872f0e3SAndreas Gohr        }
334c872f0e3SAndreas Gohr
335a1128cc0SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
336a1128cc0SAndreas Gohr            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
337a1128cc0SAndreas Gohr        }
338a1128cc0SAndreas Gohr
339c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($msg), -1);
340a1128cc0SAndreas Gohr    }
341a1128cc0SAndreas Gohr
342a1128cc0SAndreas Gohr    /**
343da369b60SAndreas Gohr     * Handle debug output
344da369b60SAndreas Gohr     *
345da369b60SAndreas Gohr     * @param string $msg
346da369b60SAndreas Gohr     * @param string $file
347da369b60SAndreas Gohr     * @param int $line
348da369b60SAndreas Gohr     */
349da369b60SAndreas Gohr    protected function debug($msg, $file, $line)
350da369b60SAndreas Gohr    {
351c872f0e3SAndreas Gohr        global $conf;
352c872f0e3SAndreas Gohr
353c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
354c872f0e3SAndreas Gohr            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
355c872f0e3SAndreas Gohr        }
356c872f0e3SAndreas Gohr
357c872f0e3SAndreas Gohr        if ($conf['allowdebug']) {
35885916a2dSAndreas Gohr            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
3591078ec26SAndreas Gohr        }
3601078ec26SAndreas Gohr    }
361c872f0e3SAndreas Gohr}
362