xref: /plugin/pureldap/classes/Client.php (revision c2500b4410d7943a5b4952a0bb25d20d1a95cb79)
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);
431078ec26SAndreas Gohr        $this->ldap = new LdapClient($this->config);
441078ec26SAndreas Gohr    }
451078ec26SAndreas Gohr
461078ec26SAndreas Gohr    /**
471078ec26SAndreas Gohr     * Setup sane config defaults
481078ec26SAndreas Gohr     *
491078ec26SAndreas Gohr     * @param array $config
501078ec26SAndreas Gohr     * @return array
511078ec26SAndreas Gohr     */
521078ec26SAndreas Gohr    protected function prepareConfig($config)
531078ec26SAndreas Gohr    {
541078ec26SAndreas Gohr        $defaults = [
551078ec26SAndreas Gohr            'defaultgroup' => 'user', // we expect this to be passed from global conf
56a1128cc0SAndreas Gohr            'suffix' => '',
571078ec26SAndreas Gohr            'port' => '',
586d90d5c8SAndreas Gohr            'encryption' => false,
591078ec26SAndreas Gohr            'admin_username' => '',
601078ec26SAndreas Gohr            'admin_password' => '',
61b914569fSAndreas Gohr            'page_size' => 150,
626d90d5c8SAndreas Gohr            'use_ssl' => false,
636d90d5c8SAndreas Gohr            'validate' => 'strict',
64*c2500b44SAndreas Gohr            'primarygroup' => 'domain users',
65b914569fSAndreas Gohr            'attributes' => [],
661078ec26SAndreas Gohr        ];
671078ec26SAndreas Gohr
681078ec26SAndreas Gohr        $config = array_merge($defaults, $config);
691078ec26SAndreas Gohr
701078ec26SAndreas Gohr        // default port depends on SSL setting
711078ec26SAndreas Gohr        if (!$config['port']) {
726d90d5c8SAndreas Gohr            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
736d90d5c8SAndreas Gohr        }
746d90d5c8SAndreas Gohr
756d90d5c8SAndreas Gohr        // set ssl parameters
766d90d5c8SAndreas Gohr        $config['use_ssl'] = ($config['encryption'] === 'ssl');
776d90d5c8SAndreas Gohr        if ($config['validate'] === 'none') {
786d90d5c8SAndreas Gohr            $config['ssl_validate_cert'] = false;
796d90d5c8SAndreas Gohr        } elseif ($config['validate'] === 'self') {
806d90d5c8SAndreas Gohr            $config['ssl_allow_self_signed'] = true;
811078ec26SAndreas Gohr        }
821078ec26SAndreas Gohr
83a1128cc0SAndreas Gohr        $config['suffix'] = PhpString::strtolower($config['suffix']);
84*c2500b44SAndreas Gohr        $config['primarygroup'] = $this->cleanGroup($config['primarygroup']);
8580ac552fSAndreas Gohr
861078ec26SAndreas Gohr        return $config;
871078ec26SAndreas Gohr    }
881078ec26SAndreas Gohr
891078ec26SAndreas Gohr    /**
901078ec26SAndreas Gohr     * Authenticate as admin
911078ec26SAndreas Gohr     */
921078ec26SAndreas Gohr    public function autoAuth()
931078ec26SAndreas Gohr    {
941078ec26SAndreas Gohr        if ($this->isAuthenticated) return true;
959446f9efSAndreas Gohr
96a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($this->config['admin_username']);
979446f9efSAndreas Gohr        $ok = $this->authenticate($user, $this->config['admin_password']);
988b2677edSAndreas Gohr        if (!$ok) {
99a1128cc0SAndreas Gohr            $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
1008b2677edSAndreas Gohr        }
1018b2677edSAndreas Gohr        return $ok;
1021078ec26SAndreas Gohr    }
1031078ec26SAndreas Gohr
1041078ec26SAndreas Gohr    /**
1051078ec26SAndreas Gohr     * Authenticates a given user. This client will remain authenticated
1061078ec26SAndreas Gohr     *
1071078ec26SAndreas Gohr     * @param string $user
1081078ec26SAndreas Gohr     * @param string $pass
1091078ec26SAndreas Gohr     * @return bool was the authentication successful?
1105a3b9122SAndreas Gohr     * @noinspection PhpRedundantCatchClauseInspection
1111078ec26SAndreas Gohr     */
1121078ec26SAndreas Gohr    public function authenticate($user, $pass)
1131078ec26SAndreas Gohr    {
114a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($user);
11580ac552fSAndreas Gohr
1166d90d5c8SAndreas Gohr        if ($this->config['encryption'] === 'tls') {
1171078ec26SAndreas Gohr            try {
1181078ec26SAndreas Gohr                $this->ldap->startTls();
1191078ec26SAndreas Gohr            } catch (OperationException $e) {
120da369b60SAndreas Gohr                $this->fatal($e);
1211078ec26SAndreas Gohr            }
1221078ec26SAndreas Gohr        }
1231078ec26SAndreas Gohr
1241078ec26SAndreas Gohr        try {
1251078ec26SAndreas Gohr            $this->ldap->bind($user, $pass);
1261078ec26SAndreas Gohr        } catch (BindException $e) {
1271078ec26SAndreas Gohr            return false;
1281078ec26SAndreas Gohr        } catch (ConnectionException $e) {
129da369b60SAndreas Gohr            $this->fatal($e);
1301078ec26SAndreas Gohr            return false;
1311078ec26SAndreas Gohr        } catch (OperationException $e) {
132da369b60SAndreas Gohr            $this->fatal($e);
1331078ec26SAndreas Gohr            return false;
1341078ec26SAndreas Gohr        }
1351078ec26SAndreas Gohr
1361078ec26SAndreas Gohr        $this->isAuthenticated = true;
1371078ec26SAndreas Gohr        return true;
1381078ec26SAndreas Gohr    }
1391078ec26SAndreas Gohr
1401078ec26SAndreas Gohr    /**
1411078ec26SAndreas Gohr     * Get info for a single user, use cache if available
1421078ec26SAndreas Gohr     *
1431078ec26SAndreas Gohr     * @param string $username
1441078ec26SAndreas Gohr     * @param bool $fetchgroups Are groups needed?
1451078ec26SAndreas Gohr     * @return array|null
1461078ec26SAndreas Gohr     */
1471078ec26SAndreas Gohr    public function getCachedUser($username, $fetchgroups = true)
1481078ec26SAndreas Gohr    {
1496d90d5c8SAndreas Gohr        global $conf;
1506d90d5c8SAndreas Gohr
1516d90d5c8SAndreas Gohr        // memory cache first
1521078ec26SAndreas Gohr        if (isset($this->userCache[$username])) {
1531078ec26SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1541078ec26SAndreas Gohr                return $this->userCache[$username];
1551078ec26SAndreas Gohr            }
1561078ec26SAndreas Gohr        }
1571078ec26SAndreas Gohr
1586d90d5c8SAndreas Gohr        // disk cache second
1596d90d5c8SAndreas Gohr        $cachename = getCacheName($username, '.pureldap-user');
1606d90d5c8SAndreas Gohr        $cachetime = @filemtime($cachename);
1616d90d5c8SAndreas Gohr        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
1626d90d5c8SAndreas Gohr            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
1636d90d5c8SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1646d90d5c8SAndreas Gohr                return $this->userCache[$username];
1656d90d5c8SAndreas Gohr            }
1666d90d5c8SAndreas Gohr        }
1676d90d5c8SAndreas Gohr
1681078ec26SAndreas Gohr        // fetch fresh data
1691078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
1701078ec26SAndreas Gohr
1711078ec26SAndreas Gohr        // store in cache
1721078ec26SAndreas Gohr        if ($info !== null) {
1731078ec26SAndreas Gohr            $this->userCache[$username] = $info;
1746d90d5c8SAndreas Gohr            file_put_contents($cachename, json_encode($info));
1751078ec26SAndreas Gohr        }
1761078ec26SAndreas Gohr
1771078ec26SAndreas Gohr        return $info;
1781078ec26SAndreas Gohr    }
1791078ec26SAndreas Gohr
1801078ec26SAndreas Gohr    /**
1811078ec26SAndreas Gohr     * Fetch a single user
1821078ec26SAndreas Gohr     *
1831078ec26SAndreas Gohr     * @param string $username
1841078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
1851078ec26SAndreas Gohr     * @return null|array
1861078ec26SAndreas Gohr     */
1871078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
1881078ec26SAndreas Gohr
1891078ec26SAndreas Gohr    /**
1905a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
1915a3b9122SAndreas Gohr     *
1925a3b9122SAndreas Gohr     * @return string[]
1935a3b9122SAndreas Gohr     */
1945a3b9122SAndreas Gohr    public function getCachedGroups()
1955a3b9122SAndreas Gohr    {
1965a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
1975a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
1985a3b9122SAndreas Gohr        }
1995a3b9122SAndreas Gohr
2005a3b9122SAndreas Gohr        return $this->groupCache;
2015a3b9122SAndreas Gohr    }
2025a3b9122SAndreas Gohr
2035a3b9122SAndreas Gohr    /**
2045a3b9122SAndreas Gohr     * Return a list of all available groups
2055a3b9122SAndreas Gohr     *
206b21740b4SAndreas Gohr     * Optionally filter the list
207b21740b4SAndreas Gohr     *
208b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
209b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2105a3b9122SAndreas Gohr     * @return string[]
2115a3b9122SAndreas Gohr     */
212204fba68SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
2135a3b9122SAndreas Gohr
21480ac552fSAndreas Gohr    /**
215a1128cc0SAndreas Gohr     * Clean the user name for use in DokuWiki
21680ac552fSAndreas Gohr     *
217a1128cc0SAndreas Gohr     * @param string $user
21880ac552fSAndreas Gohr     * @return string
21980ac552fSAndreas Gohr     */
220a1128cc0SAndreas Gohr    abstract public function cleanUser($user);
22180ac552fSAndreas Gohr
22280ac552fSAndreas Gohr    /**
223a1128cc0SAndreas Gohr     * Clean the group name for use in DokuWiki
22480ac552fSAndreas Gohr     *
225a1128cc0SAndreas Gohr     * @param string $group
22680ac552fSAndreas Gohr     * @return string
22780ac552fSAndreas Gohr     */
228a1128cc0SAndreas Gohr    abstract public function cleanGroup($group);
229a1128cc0SAndreas Gohr
230a1128cc0SAndreas Gohr    /**
231a1128cc0SAndreas Gohr     * Inheriting classes may want to manipulate the user before binding
232a1128cc0SAndreas Gohr     *
233a1128cc0SAndreas Gohr     * @param string $user
234a1128cc0SAndreas Gohr     * @return string
235a1128cc0SAndreas Gohr     */
236a1128cc0SAndreas Gohr    protected function prepareBindUser($user)
237a1128cc0SAndreas Gohr    {
238a1128cc0SAndreas Gohr        return $user;
239a1128cc0SAndreas Gohr    }
24080ac552fSAndreas Gohr
2415a3b9122SAndreas Gohr    /**
2421078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
2431078ec26SAndreas Gohr     *
2441078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
2451078ec26SAndreas Gohr     *
2461078ec26SAndreas Gohr     * @param Attribute|null $attribute
2471078ec26SAndreas Gohr     * @return string
2481078ec26SAndreas Gohr     */
2495a3b9122SAndreas Gohr    protected function attr2str($attribute)
2505a3b9122SAndreas Gohr    {
2511078ec26SAndreas Gohr        if ($attribute !== null) {
2521078ec26SAndreas Gohr            return $attribute->firstValue();
2531078ec26SAndreas Gohr        }
2541078ec26SAndreas Gohr        return '';
2551078ec26SAndreas Gohr    }
2561078ec26SAndreas Gohr
2571078ec26SAndreas Gohr    /**
2589c590892SAndreas Gohr     * Get the attributes that should be fetched for a user
2599c590892SAndreas Gohr     *
2609c590892SAndreas Gohr     * Can be extended in sub classes
2619c590892SAndreas Gohr     *
2629c590892SAndreas Gohr     * @return Attribute[]
2639c590892SAndreas Gohr     */
2649c590892SAndreas Gohr    protected function userAttributes()
2659c590892SAndreas Gohr    {
2669c590892SAndreas Gohr        // defaults
2679c590892SAndreas Gohr        $attr = [
2689c590892SAndreas Gohr            new Attribute('dn'),
2699c590892SAndreas Gohr            new Attribute('displayName'),
2709c590892SAndreas Gohr            new Attribute('mail'),
2719c590892SAndreas Gohr        ];
2729c590892SAndreas Gohr        // additionals
2739c590892SAndreas Gohr        foreach ($this->config['attributes'] as $attribute) {
2749c590892SAndreas Gohr            $attr[] = new Attribute($attribute);
2759c590892SAndreas Gohr        }
2769c590892SAndreas Gohr        return $attr;
2779c590892SAndreas Gohr    }
2789c590892SAndreas Gohr
2799c590892SAndreas Gohr    /**
280da369b60SAndreas Gohr     * Handle fatal exceptions
2811078ec26SAndreas Gohr     *
2821078ec26SAndreas Gohr     * @param \Exception $e
2831078ec26SAndreas Gohr     */
284da369b60SAndreas Gohr    protected function fatal(\Exception $e)
2851078ec26SAndreas Gohr    {
286c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\ErrorHandler')) {
287c872f0e3SAndreas Gohr            \dokuwiki\ErrorHandler::logException($e);
288c872f0e3SAndreas Gohr        }
289c872f0e3SAndreas Gohr
2901078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
2918595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
2921078ec26SAndreas Gohr        }
293c872f0e3SAndreas Gohr
294c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()), -1);
295da369b60SAndreas Gohr    }
2961078ec26SAndreas Gohr
297da369b60SAndreas Gohr    /**
298a1128cc0SAndreas Gohr     * Handle error output
299a1128cc0SAndreas Gohr     *
300a1128cc0SAndreas Gohr     * @param string $msg
301a1128cc0SAndreas Gohr     * @param string $file
302a1128cc0SAndreas Gohr     * @param int $line
303a1128cc0SAndreas Gohr     */
304a1128cc0SAndreas Gohr    protected function error($msg, $file, $line)
305a1128cc0SAndreas Gohr    {
306c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
307c872f0e3SAndreas Gohr            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
308c872f0e3SAndreas Gohr        }
309c872f0e3SAndreas Gohr
310a1128cc0SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
311a1128cc0SAndreas Gohr            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
312a1128cc0SAndreas Gohr        }
313a1128cc0SAndreas Gohr
314c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($msg), -1);
315a1128cc0SAndreas Gohr    }
316a1128cc0SAndreas Gohr
317a1128cc0SAndreas Gohr    /**
318da369b60SAndreas Gohr     * Handle debug output
319da369b60SAndreas Gohr     *
320da369b60SAndreas Gohr     * @param string $msg
321da369b60SAndreas Gohr     * @param string $file
322da369b60SAndreas Gohr     * @param int $line
323da369b60SAndreas Gohr     */
324da369b60SAndreas Gohr    protected function debug($msg, $file, $line)
325da369b60SAndreas Gohr    {
326c872f0e3SAndreas Gohr        global $conf;
327c872f0e3SAndreas Gohr
328c872f0e3SAndreas Gohr        if (class_exists('\dokuwiki\Logger')) {
329c872f0e3SAndreas Gohr            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
330c872f0e3SAndreas Gohr        }
331c872f0e3SAndreas Gohr
332c872f0e3SAndreas Gohr        if ($conf['allowdebug']) {
33385916a2dSAndreas Gohr            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
3341078ec26SAndreas Gohr        }
3351078ec26SAndreas Gohr    }
336c872f0e3SAndreas Gohr}
337