xref: /plugin/pureldap/classes/Client.php (revision 08ace392be71b69ddc8b1eda246fad47272b7606)
11078ec26SAndreas Gohr<?php
21078ec26SAndreas Gohr
31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes;
41078ec26SAndreas Gohr
5*08ace392SAndreas Gohruse dokuwiki\ErrorHandler;
6*08ace392SAndreas Gohruse dokuwiki\Logger;
7*08ace392SAndreas Gohruse dokuwiki\Utf8\Clean;
880ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString;
91078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute;
101078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\BindException;
111078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\ConnectionException;
121078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException;
131078ec26SAndreas Gohruse FreeDSx\Ldap\LdapClient;
141078ec26SAndreas Gohr
151078ec26SAndreas Gohrrequire_once __DIR__ . '/../vendor/autoload.php';
161078ec26SAndreas Gohr
171078ec26SAndreas Gohrabstract class Client
181078ec26SAndreas Gohr{
19204fba68SAndreas Gohr    const FILTER_EQUAL = 'equal';
20204fba68SAndreas Gohr    const FILTER_CONTAINS = 'contains';
21204fba68SAndreas Gohr    const FILTER_STARTSWITH = 'startsWith';
22204fba68SAndreas Gohr    const FILTER_ENDSWITH = 'endsWith';
23204fba68SAndreas Gohr
241078ec26SAndreas Gohr    /** @var array the configuration */
251078ec26SAndreas Gohr    protected $config;
261078ec26SAndreas Gohr
271078ec26SAndreas Gohr    /** @var LdapClient */
281078ec26SAndreas Gohr    protected $ldap;
291078ec26SAndreas Gohr
30*08ace392SAndreas Gohr    /** @var bool|string is this client authenticated already? Contains username if yes */
311078ec26SAndreas Gohr    protected $isAuthenticated = false;
321078ec26SAndreas Gohr
331078ec26SAndreas Gohr    /** @var array cached user info */
341078ec26SAndreas Gohr    protected $userCache = [];
351078ec26SAndreas Gohr
365a3b9122SAndreas Gohr    /** @var array cached group list */
375a3b9122SAndreas Gohr    protected $groupCache = [];
385a3b9122SAndreas Gohr
391078ec26SAndreas Gohr    /**
401078ec26SAndreas Gohr     * Client constructor.
411078ec26SAndreas Gohr     * @param array $config
421078ec26SAndreas Gohr     */
431078ec26SAndreas Gohr    public function __construct($config)
441078ec26SAndreas Gohr    {
451078ec26SAndreas Gohr        $this->config = $this->prepareConfig($config);
46bf69b89cSAndreas Gohr        $this->prepareSSO();
471078ec26SAndreas Gohr        $this->ldap = new LdapClient($this->config);
481078ec26SAndreas Gohr    }
491078ec26SAndreas Gohr
501078ec26SAndreas Gohr    /**
511078ec26SAndreas Gohr     * Setup sane config defaults
521078ec26SAndreas Gohr     *
531078ec26SAndreas Gohr     * @param array $config
541078ec26SAndreas Gohr     * @return array
551078ec26SAndreas Gohr     */
561078ec26SAndreas Gohr    protected function prepareConfig($config)
571078ec26SAndreas Gohr    {
581a4f0e1fSAndreas Gohr        // ensure we have the default keys
591a4f0e1fSAndreas Gohr        /** @var array $conf */
601a4f0e1fSAndreas Gohr        include __DIR__ . '/../conf/default.php';
611a4f0e1fSAndreas Gohr        $defaults = $conf;
621a4f0e1fSAndreas Gohr        $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf
631078ec26SAndreas Gohr
641078ec26SAndreas Gohr        $config = array_merge($defaults, $config);
651078ec26SAndreas Gohr
661078ec26SAndreas Gohr        // default port depends on SSL setting
671078ec26SAndreas Gohr        if (!$config['port']) {
686d90d5c8SAndreas Gohr            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
696d90d5c8SAndreas Gohr        }
706d90d5c8SAndreas Gohr
716d90d5c8SAndreas Gohr        // set ssl parameters
726d90d5c8SAndreas Gohr        $config['use_ssl'] = ($config['encryption'] === 'ssl');
736d90d5c8SAndreas Gohr        if ($config['validate'] === 'none') {
746d90d5c8SAndreas Gohr            $config['ssl_validate_cert'] = false;
756d90d5c8SAndreas Gohr        } elseif ($config['validate'] === 'self') {
766d90d5c8SAndreas Gohr            $config['ssl_allow_self_signed'] = true;
771078ec26SAndreas Gohr        }
781078ec26SAndreas Gohr
79a1128cc0SAndreas Gohr        $config['suffix'] = PhpString::strtolower($config['suffix']);
80c2500b44SAndreas Gohr        $config['primarygroup'] = $this->cleanGroup($config['primarygroup']);
8180ac552fSAndreas Gohr
821078ec26SAndreas Gohr        return $config;
831078ec26SAndreas Gohr    }
841078ec26SAndreas Gohr
851078ec26SAndreas Gohr    /**
86bf69b89cSAndreas Gohr     * Extract user info from environment for SSO
87bf69b89cSAndreas Gohr     */
88bf69b89cSAndreas Gohr    protected function prepareSSO()
89bf69b89cSAndreas Gohr    {
90bf69b89cSAndreas Gohr        global $INPUT;
91bf69b89cSAndreas Gohr
92bf69b89cSAndreas Gohr        if (!$this->config['sso']) return;
93bf69b89cSAndreas Gohr        if ($INPUT->server->str('REMOTE_USER') === '') return;
94bf69b89cSAndreas Gohr
95bf69b89cSAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
96bf69b89cSAndreas Gohr
97bf69b89cSAndreas Gohr        // make sure the right encoding is used
98bf69b89cSAndreas Gohr        if ($this->config['sso_charset']) {
99*08ace392SAndreas Gohr            if (function_exists('iconv')) {
100bf69b89cSAndreas Gohr                $user = iconv($this->config['sso_charset'], 'UTF-8', $user);
101*08ace392SAndreas Gohr            } elseif (function_exists('mb_convert_encoding')) {
102*08ace392SAndreas Gohr                $user = mb_convert_encoding($user, 'UTF-8', $this->config['sso_charset']);
103*08ace392SAndreas Gohr            }
104*08ace392SAndreas Gohr        } elseif (!Clean::isUtf8($user)) {
105bf69b89cSAndreas Gohr            $user = utf8_encode($user);
106bf69b89cSAndreas Gohr        }
107bf69b89cSAndreas Gohr        $user = $this->cleanUser($user);
108bf69b89cSAndreas Gohr
109bf69b89cSAndreas Gohr        // Prepare SSO
110bf69b89cSAndreas Gohr
111bf69b89cSAndreas Gohr        // trust the incoming user
112bf69b89cSAndreas Gohr        $INPUT->server->set('REMOTE_USER', $user);
113bf69b89cSAndreas Gohr
114bf69b89cSAndreas Gohr        // we need to simulate a login
115bf69b89cSAndreas Gohr        if (empty($_COOKIE[DOKU_COOKIE])) {
116bf69b89cSAndreas Gohr            $INPUT->set('u', $user);
117bf69b89cSAndreas Gohr            $INPUT->set('p', 'sso_only');
118bf69b89cSAndreas Gohr        }
119bf69b89cSAndreas Gohr    }
120bf69b89cSAndreas Gohr
121bf69b89cSAndreas Gohr    /**
12222654fdeSAndreas Gohr     * Access to the config values
12322654fdeSAndreas Gohr     *
12422654fdeSAndreas Gohr     * Because the client class does configuration cleanup, the auth class should access
12522654fdeSAndreas Gohr     * config values through the client
12622654fdeSAndreas Gohr     *
12722654fdeSAndreas Gohr     * @param string $key
12822654fdeSAndreas Gohr     * @return mixed returns null on missing config
12922654fdeSAndreas Gohr     */
130*08ace392SAndreas Gohr    public function getConf($key)
131*08ace392SAndreas Gohr    {
13222654fdeSAndreas Gohr        if (!isset($this->config[$key])) return null;
13322654fdeSAndreas Gohr        return $this->config[$key];
13422654fdeSAndreas Gohr    }
13522654fdeSAndreas Gohr
13622654fdeSAndreas Gohr    /**
137*08ace392SAndreas Gohr     * Authenticate as admin if not authenticated yet
1381078ec26SAndreas Gohr     */
1391078ec26SAndreas Gohr    public function autoAuth()
1401078ec26SAndreas Gohr    {
1411078ec26SAndreas Gohr        if ($this->isAuthenticated) return true;
1429446f9efSAndreas Gohr
143a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($this->config['admin_username']);
1449446f9efSAndreas Gohr        $ok = $this->authenticate($user, $this->config['admin_password']);
1458b2677edSAndreas Gohr        if (!$ok) {
146*08ace392SAndreas Gohr            $this->error('Automatic bind failed. Probably wrong user/password.', __FILE__, __LINE__);
1478b2677edSAndreas Gohr        }
1488b2677edSAndreas Gohr        return $ok;
1491078ec26SAndreas Gohr    }
1501078ec26SAndreas Gohr
1511078ec26SAndreas Gohr    /**
1521078ec26SAndreas Gohr     * Authenticates a given user. This client will remain authenticated
1531078ec26SAndreas Gohr     *
1541078ec26SAndreas Gohr     * @param string $user
1551078ec26SAndreas Gohr     * @param string $pass
1561078ec26SAndreas Gohr     * @return bool was the authentication successful?
1575a3b9122SAndreas Gohr     * @noinspection PhpRedundantCatchClauseInspection
1581078ec26SAndreas Gohr     */
1591078ec26SAndreas Gohr    public function authenticate($user, $pass)
1601078ec26SAndreas Gohr    {
161a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($user);
162*08ace392SAndreas Gohr        $this->isAuthenticated = false;
16380ac552fSAndreas Gohr
164*08ace392SAndreas Gohr        if (!$this->ldap->isConnected() && $this->config['encryption'] === 'tls') {
1651078ec26SAndreas Gohr            try {
1661078ec26SAndreas Gohr                $this->ldap->startTls();
167*08ace392SAndreas Gohr            } catch (ConnectionException|OperationException $e) {
168da369b60SAndreas Gohr                $this->fatal($e);
169*08ace392SAndreas Gohr                return false;
1701078ec26SAndreas Gohr            }
1711078ec26SAndreas Gohr        }
1721078ec26SAndreas Gohr
1731078ec26SAndreas Gohr        try {
1741078ec26SAndreas Gohr            $this->ldap->bind($user, $pass);
1751078ec26SAndreas Gohr        } catch (BindException $e) {
176*08ace392SAndreas Gohr            $this->debug('Bind failed: ' . $e->getMessage(), $e->getFile(), $e->getLine());
1771078ec26SAndreas Gohr            return false;
178*08ace392SAndreas Gohr        } catch (ConnectionException|OperationException $e) {
179da369b60SAndreas Gohr            $this->fatal($e);
1801078ec26SAndreas Gohr            return false;
1811078ec26SAndreas Gohr        }
1821078ec26SAndreas Gohr
183*08ace392SAndreas Gohr        $this->isAuthenticated = $user;
1841078ec26SAndreas Gohr        return true;
1851078ec26SAndreas Gohr    }
1861078ec26SAndreas Gohr
1871078ec26SAndreas Gohr    /**
1881078ec26SAndreas Gohr     * Get info for a single user, use cache if available
1891078ec26SAndreas Gohr     *
1901078ec26SAndreas Gohr     * @param string $username
1911078ec26SAndreas Gohr     * @param bool $fetchgroups Are groups needed?
1921078ec26SAndreas Gohr     * @return array|null
1931078ec26SAndreas Gohr     */
1941078ec26SAndreas Gohr    public function getCachedUser($username, $fetchgroups = true)
1951078ec26SAndreas Gohr    {
1966d90d5c8SAndreas Gohr        global $conf;
1976d90d5c8SAndreas Gohr
1986d90d5c8SAndreas Gohr        // memory cache first
1991078ec26SAndreas Gohr        if (isset($this->userCache[$username])) {
2001078ec26SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
2011078ec26SAndreas Gohr                return $this->userCache[$username];
2021078ec26SAndreas Gohr            }
2031078ec26SAndreas Gohr        }
2041078ec26SAndreas Gohr
2056d90d5c8SAndreas Gohr        // disk cache second
2065dcabedaSAndreas Gohr        if ($this->config['usefscache']) {
2076d90d5c8SAndreas Gohr            $cachename = getCacheName($username, '.pureldap-user');
2086d90d5c8SAndreas Gohr            $cachetime = @filemtime($cachename);
2096d90d5c8SAndreas Gohr            if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
2106d90d5c8SAndreas Gohr                $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
2116d90d5c8SAndreas Gohr                if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
2126d90d5c8SAndreas Gohr                    return $this->userCache[$username];
2136d90d5c8SAndreas Gohr                }
2146d90d5c8SAndreas Gohr            }
2155dcabedaSAndreas Gohr        }
2166d90d5c8SAndreas Gohr
2171078ec26SAndreas Gohr        // fetch fresh data
2181078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
2191078ec26SAndreas Gohr
2201078ec26SAndreas Gohr        // store in cache
2215dcabedaSAndreas Gohr        if ($this->config['usefscache'] && $info !== null) {
2221078ec26SAndreas Gohr            $this->userCache[$username] = $info;
223*08ace392SAndreas Gohr            /** @noinspection PhpUndefinedVariableInspection We know that cachename is defined */
2246d90d5c8SAndreas Gohr            file_put_contents($cachename, json_encode($info));
2251078ec26SAndreas Gohr        }
2261078ec26SAndreas Gohr
2271078ec26SAndreas Gohr        return $info;
2281078ec26SAndreas Gohr    }
2291078ec26SAndreas Gohr
2301078ec26SAndreas Gohr    /**
2311078ec26SAndreas Gohr     * Fetch a single user
2321078ec26SAndreas Gohr     *
2331078ec26SAndreas Gohr     * @param string $username
2341078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
2351078ec26SAndreas Gohr     * @return null|array
2361078ec26SAndreas Gohr     */
2371078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
2381078ec26SAndreas Gohr
2391078ec26SAndreas Gohr    /**
240*08ace392SAndreas Gohr     * Set a new password for a user
241*08ace392SAndreas Gohr     *
242*08ace392SAndreas Gohr     * @param string $username
243*08ace392SAndreas Gohr     * @param string $newpass
244*08ace392SAndreas Gohr     * @param string $oldpass Needed for self-service password change in AD
245*08ace392SAndreas Gohr     * @return bool
246*08ace392SAndreas Gohr     */
247*08ace392SAndreas Gohr    abstract public function setPassword($username, $newpass, $oldpass = null);
248*08ace392SAndreas Gohr
249*08ace392SAndreas Gohr    /**
2505a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
2515a3b9122SAndreas Gohr     *
2525a3b9122SAndreas Gohr     * @return string[]
2535a3b9122SAndreas Gohr     */
2545a3b9122SAndreas Gohr    public function getCachedGroups()
2555a3b9122SAndreas Gohr    {
2565a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
2575a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
2585a3b9122SAndreas Gohr        }
2595a3b9122SAndreas Gohr
2605a3b9122SAndreas Gohr        return $this->groupCache;
2615a3b9122SAndreas Gohr    }
2625a3b9122SAndreas Gohr
2635a3b9122SAndreas Gohr    /**
2645a3b9122SAndreas Gohr     * Return a list of all available groups
2655a3b9122SAndreas Gohr     *
266b21740b4SAndreas Gohr     * Optionally filter the list
267b21740b4SAndreas Gohr     *
268b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
269b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2705a3b9122SAndreas Gohr     * @return string[]
2715a3b9122SAndreas Gohr     */
272204fba68SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
2735a3b9122SAndreas Gohr
27480ac552fSAndreas Gohr    /**
275a1128cc0SAndreas Gohr     * Clean the user name for use in DokuWiki
27680ac552fSAndreas Gohr     *
277a1128cc0SAndreas Gohr     * @param string $user
27880ac552fSAndreas Gohr     * @return string
27980ac552fSAndreas Gohr     */
280a1128cc0SAndreas Gohr    abstract public function cleanUser($user);
28180ac552fSAndreas Gohr
28280ac552fSAndreas Gohr    /**
283a1128cc0SAndreas Gohr     * Clean the group name for use in DokuWiki
28480ac552fSAndreas Gohr     *
285a1128cc0SAndreas Gohr     * @param string $group
28680ac552fSAndreas Gohr     * @return string
28780ac552fSAndreas Gohr     */
288a1128cc0SAndreas Gohr    abstract public function cleanGroup($group);
289a1128cc0SAndreas Gohr
290a1128cc0SAndreas Gohr    /**
291a1128cc0SAndreas Gohr     * Inheriting classes may want to manipulate the user before binding
292a1128cc0SAndreas Gohr     *
293a1128cc0SAndreas Gohr     * @param string $user
294a1128cc0SAndreas Gohr     * @return string
295a1128cc0SAndreas Gohr     */
296a1128cc0SAndreas Gohr    protected function prepareBindUser($user)
297a1128cc0SAndreas Gohr    {
298a1128cc0SAndreas Gohr        return $user;
299a1128cc0SAndreas Gohr    }
30080ac552fSAndreas Gohr
3015a3b9122SAndreas Gohr    /**
3021078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
3031078ec26SAndreas Gohr     *
3041078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
3051078ec26SAndreas Gohr     *
3061078ec26SAndreas Gohr     * @param Attribute|null $attribute
3071078ec26SAndreas Gohr     * @return string
3081078ec26SAndreas Gohr     */
3095a3b9122SAndreas Gohr    protected function attr2str($attribute)
3105a3b9122SAndreas Gohr    {
3111078ec26SAndreas Gohr        if ($attribute !== null) {
3121078ec26SAndreas Gohr            return $attribute->firstValue();
3131078ec26SAndreas Gohr        }
3141078ec26SAndreas Gohr        return '';
3151078ec26SAndreas Gohr    }
3161078ec26SAndreas Gohr
3171078ec26SAndreas Gohr    /**
3189c590892SAndreas Gohr     * Get the attributes that should be fetched for a user
3199c590892SAndreas Gohr     *
3209c590892SAndreas Gohr     * Can be extended in sub classes
3219c590892SAndreas Gohr     *
3229c590892SAndreas Gohr     * @return Attribute[]
3239c590892SAndreas Gohr     */
3249c590892SAndreas Gohr    protected function userAttributes()
3259c590892SAndreas Gohr    {
3269c590892SAndreas Gohr        // defaults
3279c590892SAndreas Gohr        $attr = [
3289c590892SAndreas Gohr            new Attribute('dn'),
3299c590892SAndreas Gohr            new Attribute('displayName'),
3309c590892SAndreas Gohr            new Attribute('mail'),
3319c590892SAndreas Gohr        ];
3329c590892SAndreas Gohr        // additionals
3339c590892SAndreas Gohr        foreach ($this->config['attributes'] as $attribute) {
3349c590892SAndreas Gohr            $attr[] = new Attribute($attribute);
3359c590892SAndreas Gohr        }
3369c590892SAndreas Gohr        return $attr;
3379c590892SAndreas Gohr    }
3389c590892SAndreas Gohr
3399c590892SAndreas Gohr    /**
340da369b60SAndreas Gohr     * Handle fatal exceptions
3411078ec26SAndreas Gohr     *
3421078ec26SAndreas Gohr     * @param \Exception $e
3431078ec26SAndreas Gohr     */
344da369b60SAndreas Gohr    protected function fatal(\Exception $e)
3451078ec26SAndreas Gohr    {
346*08ace392SAndreas Gohr        ErrorHandler::logException($e);
347c872f0e3SAndreas Gohr
3481078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
3498595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
3501078ec26SAndreas Gohr        }
351c872f0e3SAndreas Gohr
352c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()), -1);
353da369b60SAndreas Gohr    }
3541078ec26SAndreas Gohr
355da369b60SAndreas Gohr    /**
356a1128cc0SAndreas Gohr     * Handle error output
357a1128cc0SAndreas Gohr     *
358a1128cc0SAndreas Gohr     * @param string $msg
359a1128cc0SAndreas Gohr     * @param string $file
360a1128cc0SAndreas Gohr     * @param int $line
361a1128cc0SAndreas Gohr     */
362*08ace392SAndreas Gohr    public function error($msg, $file, $line)
363a1128cc0SAndreas Gohr    {
364*08ace392SAndreas Gohr        Logger::error('[pureldap] ' . $msg, '', $file, $line);
365c872f0e3SAndreas Gohr
366a1128cc0SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
367a1128cc0SAndreas Gohr            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
368a1128cc0SAndreas Gohr        }
369a1128cc0SAndreas Gohr
370c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($msg), -1);
371a1128cc0SAndreas Gohr    }
372a1128cc0SAndreas Gohr
373a1128cc0SAndreas Gohr    /**
374da369b60SAndreas Gohr     * Handle debug output
375da369b60SAndreas Gohr     *
376da369b60SAndreas Gohr     * @param string $msg
377da369b60SAndreas Gohr     * @param string $file
378da369b60SAndreas Gohr     * @param int $line
379da369b60SAndreas Gohr     */
380*08ace392SAndreas Gohr    public function debug($msg, $file, $line)
381da369b60SAndreas Gohr    {
382c872f0e3SAndreas Gohr        global $conf;
383c872f0e3SAndreas Gohr
384*08ace392SAndreas Gohr        Logger::debug('[pureldap] ' . $msg, '', $file, $line);
385c872f0e3SAndreas Gohr
386c872f0e3SAndreas Gohr        if ($conf['allowdebug']) {
387*08ace392SAndreas Gohr            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line);
3881078ec26SAndreas Gohr        }
3891078ec26SAndreas Gohr    }
390c872f0e3SAndreas Gohr}
391