xref: /plugin/pureldap/classes/Client.php (revision b914569fe2bf0cc586f78951bd0c636bc6597916)
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{
161078ec26SAndreas Gohr    /** @var array the configuration */
171078ec26SAndreas Gohr    protected $config;
181078ec26SAndreas Gohr
191078ec26SAndreas Gohr    /** @var LdapClient */
201078ec26SAndreas Gohr    protected $ldap;
211078ec26SAndreas Gohr
221078ec26SAndreas Gohr    /** @var bool is this client authenticated already? */
231078ec26SAndreas Gohr    protected $isAuthenticated = false;
241078ec26SAndreas Gohr
251078ec26SAndreas Gohr    /** @var array cached user info */
261078ec26SAndreas Gohr    protected $userCache = [];
271078ec26SAndreas Gohr
285a3b9122SAndreas Gohr    /** @var array cached group list */
295a3b9122SAndreas Gohr    protected $groupCache = [];
305a3b9122SAndreas Gohr
311078ec26SAndreas Gohr    /**
321078ec26SAndreas Gohr     * Client constructor.
331078ec26SAndreas Gohr     * @param array $config
341078ec26SAndreas Gohr     */
351078ec26SAndreas Gohr    public function __construct($config)
361078ec26SAndreas Gohr    {
371078ec26SAndreas Gohr        $this->config = $this->prepareConfig($config);
381078ec26SAndreas Gohr        $this->ldap = new LdapClient($this->config);
391078ec26SAndreas Gohr    }
401078ec26SAndreas Gohr
411078ec26SAndreas Gohr    /**
421078ec26SAndreas Gohr     * Setup sane config defaults
431078ec26SAndreas Gohr     *
441078ec26SAndreas Gohr     * @param array $config
451078ec26SAndreas Gohr     * @return array
461078ec26SAndreas Gohr     */
471078ec26SAndreas Gohr    protected function prepareConfig($config)
481078ec26SAndreas Gohr    {
491078ec26SAndreas Gohr        $defaults = [
501078ec26SAndreas Gohr            'defaultgroup' => 'user', // we expect this to be passed from global conf
5180ac552fSAndreas Gohr            'domain' => '',
521078ec26SAndreas Gohr            'port' => '',
536d90d5c8SAndreas Gohr            'encryption' => false,
541078ec26SAndreas Gohr            'admin_username' => '',
551078ec26SAndreas Gohr            'admin_password' => '',
56*b914569fSAndreas Gohr            'page_size' => 150,
576d90d5c8SAndreas Gohr            'use_ssl' => false,
586d90d5c8SAndreas Gohr            'validate' => 'strict',
59*b914569fSAndreas Gohr            'attributes' => [],
601078ec26SAndreas Gohr        ];
611078ec26SAndreas Gohr
621078ec26SAndreas Gohr        $config = array_merge($defaults, $config);
631078ec26SAndreas Gohr
641078ec26SAndreas Gohr        // default port depends on SSL setting
651078ec26SAndreas Gohr        if (!$config['port']) {
666d90d5c8SAndreas Gohr            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
676d90d5c8SAndreas Gohr        }
686d90d5c8SAndreas Gohr
696d90d5c8SAndreas Gohr        // set ssl parameters
706d90d5c8SAndreas Gohr        $config['use_ssl'] = ($config['encryption'] === 'ssl');
716d90d5c8SAndreas Gohr        if ($config['validate'] === 'none') {
726d90d5c8SAndreas Gohr            $config['ssl_validate_cert'] = false;
736d90d5c8SAndreas Gohr        } elseif ($config['validate'] === 'self') {
746d90d5c8SAndreas Gohr            $config['ssl_allow_self_signed'] = true;
751078ec26SAndreas Gohr        }
761078ec26SAndreas Gohr
7780ac552fSAndreas Gohr        $config['domain'] = PhpString::strtolower($config['domain']);
7880ac552fSAndreas Gohr
791078ec26SAndreas Gohr        return $config;
801078ec26SAndreas Gohr    }
811078ec26SAndreas Gohr
821078ec26SAndreas Gohr    /**
831078ec26SAndreas Gohr     * Authenticate as admin
841078ec26SAndreas Gohr     */
851078ec26SAndreas Gohr    public function autoAuth()
861078ec26SAndreas Gohr    {
871078ec26SAndreas Gohr        if ($this->isAuthenticated) return true;
889446f9efSAndreas Gohr
899446f9efSAndreas Gohr        $user = $this->qualifiedUser($this->config['admin_username']);
909446f9efSAndreas Gohr        $ok = $this->authenticate($user, $this->config['admin_password']);
918b2677edSAndreas Gohr        if(!$ok) {
928b2677edSAndreas Gohr            $this->debug('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
938b2677edSAndreas Gohr        }
948b2677edSAndreas Gohr        return $ok;
951078ec26SAndreas Gohr    }
961078ec26SAndreas Gohr
971078ec26SAndreas Gohr    /**
981078ec26SAndreas Gohr     * Authenticates a given user. This client will remain authenticated
991078ec26SAndreas Gohr     *
1001078ec26SAndreas Gohr     * @param string $user
1011078ec26SAndreas Gohr     * @param string $pass
1021078ec26SAndreas Gohr     * @return bool was the authentication successful?
1035a3b9122SAndreas Gohr     * @noinspection PhpRedundantCatchClauseInspection
1041078ec26SAndreas Gohr     */
1051078ec26SAndreas Gohr    public function authenticate($user, $pass)
1061078ec26SAndreas Gohr    {
10780ac552fSAndreas Gohr        $user = $this->qualifiedUser($user);
10880ac552fSAndreas Gohr
1096d90d5c8SAndreas Gohr        if ($this->config['encryption'] === 'tls') {
1101078ec26SAndreas Gohr            try {
1111078ec26SAndreas Gohr                $this->ldap->startTls();
1121078ec26SAndreas Gohr            } catch (OperationException $e) {
113da369b60SAndreas Gohr                $this->fatal($e);
1141078ec26SAndreas Gohr            }
1151078ec26SAndreas Gohr        }
1161078ec26SAndreas Gohr
1171078ec26SAndreas Gohr        try {
1181078ec26SAndreas Gohr            $this->ldap->bind($user, $pass);
1191078ec26SAndreas Gohr        } catch (BindException $e) {
1201078ec26SAndreas Gohr            return false;
1211078ec26SAndreas Gohr        } catch (ConnectionException $e) {
122da369b60SAndreas Gohr            $this->fatal($e);
1231078ec26SAndreas Gohr            return false;
1241078ec26SAndreas Gohr        } catch (OperationException $e) {
125da369b60SAndreas Gohr            $this->fatal($e);
1261078ec26SAndreas Gohr            return false;
1271078ec26SAndreas Gohr        }
1281078ec26SAndreas Gohr
1291078ec26SAndreas Gohr        $this->isAuthenticated = true;
1301078ec26SAndreas Gohr        return true;
1311078ec26SAndreas Gohr    }
1321078ec26SAndreas Gohr
1331078ec26SAndreas Gohr    /**
1341078ec26SAndreas Gohr     * Get info for a single user, use cache if available
1351078ec26SAndreas Gohr     *
1361078ec26SAndreas Gohr     * @param string $username
1371078ec26SAndreas Gohr     * @param bool $fetchgroups Are groups needed?
1381078ec26SAndreas Gohr     * @return array|null
1391078ec26SAndreas Gohr     */
1401078ec26SAndreas Gohr    public function getCachedUser($username, $fetchgroups = true)
1411078ec26SAndreas Gohr    {
1426d90d5c8SAndreas Gohr        global $conf;
1436d90d5c8SAndreas Gohr
1446d90d5c8SAndreas Gohr        // memory cache first
1451078ec26SAndreas Gohr        if (isset($this->userCache[$username])) {
1461078ec26SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1471078ec26SAndreas Gohr                return $this->userCache[$username];
1481078ec26SAndreas Gohr            }
1491078ec26SAndreas Gohr        }
1501078ec26SAndreas Gohr
1516d90d5c8SAndreas Gohr        // disk cache second
1526d90d5c8SAndreas Gohr        $cachename = getCacheName($username, '.pureldap-user');
1536d90d5c8SAndreas Gohr        $cachetime = @filemtime($cachename);
1546d90d5c8SAndreas Gohr        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
1556d90d5c8SAndreas Gohr            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
1566d90d5c8SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
1576d90d5c8SAndreas Gohr                return $this->userCache[$username];
1586d90d5c8SAndreas Gohr            }
1596d90d5c8SAndreas Gohr        }
1606d90d5c8SAndreas Gohr
1611078ec26SAndreas Gohr        // fetch fresh data
1621078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
1631078ec26SAndreas Gohr
1641078ec26SAndreas Gohr        // store in cache
1651078ec26SAndreas Gohr        if ($info !== null) {
1661078ec26SAndreas Gohr            $this->userCache[$username] = $info;
1676d90d5c8SAndreas Gohr            file_put_contents($cachename, json_encode($info));
1681078ec26SAndreas Gohr        }
1691078ec26SAndreas Gohr
1701078ec26SAndreas Gohr        return $info;
1711078ec26SAndreas Gohr    }
1721078ec26SAndreas Gohr
1731078ec26SAndreas Gohr    /**
1741078ec26SAndreas Gohr     * Fetch a single user
1751078ec26SAndreas Gohr     *
1761078ec26SAndreas Gohr     * @param string $username
1771078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
1781078ec26SAndreas Gohr     * @return null|array
1791078ec26SAndreas Gohr     */
1801078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
1811078ec26SAndreas Gohr
1821078ec26SAndreas Gohr    /**
1835a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
1845a3b9122SAndreas Gohr     *
1855a3b9122SAndreas Gohr     * @return string[]
1865a3b9122SAndreas Gohr     */
1875a3b9122SAndreas Gohr    public function getCachedGroups()
1885a3b9122SAndreas Gohr    {
1895a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
1905a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
1915a3b9122SAndreas Gohr        }
1925a3b9122SAndreas Gohr
1935a3b9122SAndreas Gohr        return $this->groupCache;
1945a3b9122SAndreas Gohr    }
1955a3b9122SAndreas Gohr
1965a3b9122SAndreas Gohr    /**
1975a3b9122SAndreas Gohr     * Return a list of all available groups
1985a3b9122SAndreas Gohr     *
199b21740b4SAndreas Gohr     * Optionally filter the list
200b21740b4SAndreas Gohr     *
201b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
202b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2035a3b9122SAndreas Gohr     * @return string[]
2045a3b9122SAndreas Gohr     */
205b21740b4SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = 'equal');
2065a3b9122SAndreas Gohr
20780ac552fSAndreas Gohr    /**
20880ac552fSAndreas Gohr     * Construst the fully qualified name to identify a user
20980ac552fSAndreas Gohr     *
21080ac552fSAndreas Gohr     * @param string $username
21180ac552fSAndreas Gohr     * @return string
21280ac552fSAndreas Gohr     */
21380ac552fSAndreas Gohr    abstract public function qualifiedUser($username);
21480ac552fSAndreas Gohr
21580ac552fSAndreas Gohr    /**
21680ac552fSAndreas Gohr     * Simplify the username if possible
21780ac552fSAndreas Gohr     *
21880ac552fSAndreas Gohr     * @param string $username
21980ac552fSAndreas Gohr     * @return string
22080ac552fSAndreas Gohr     */
22180ac552fSAndreas Gohr    abstract public function simpleUser($username);
22280ac552fSAndreas Gohr
2235a3b9122SAndreas Gohr    /**
2241078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
2251078ec26SAndreas Gohr     *
2261078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
2271078ec26SAndreas Gohr     *
2281078ec26SAndreas Gohr     * @param Attribute|null $attribute
2291078ec26SAndreas Gohr     * @return string
2301078ec26SAndreas Gohr     */
2315a3b9122SAndreas Gohr    protected function attr2str($attribute)
2325a3b9122SAndreas Gohr    {
2331078ec26SAndreas Gohr        if ($attribute !== null) {
2341078ec26SAndreas Gohr            return $attribute->firstValue();
2351078ec26SAndreas Gohr        }
2361078ec26SAndreas Gohr        return '';
2371078ec26SAndreas Gohr    }
2381078ec26SAndreas Gohr
2391078ec26SAndreas Gohr    /**
240da369b60SAndreas Gohr     * Handle fatal exceptions
2411078ec26SAndreas Gohr     *
2421078ec26SAndreas Gohr     * @param \Exception $e
2431078ec26SAndreas Gohr     */
244da369b60SAndreas Gohr    protected function fatal(\Exception $e)
2451078ec26SAndreas Gohr    {
2461078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
2478595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
2481078ec26SAndreas Gohr        }
249da369b60SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()) . ' at ' . $e->getFile() . ':' . $e->getLine(), -1);
250da369b60SAndreas Gohr    }
2511078ec26SAndreas Gohr
252da369b60SAndreas Gohr    /**
253da369b60SAndreas Gohr     * Handle debug output
254da369b60SAndreas Gohr     *
255da369b60SAndreas Gohr     * @param string $msg
256da369b60SAndreas Gohr     * @param string $file
257da369b60SAndreas Gohr     * @param int $line
258da369b60SAndreas Gohr     */
259da369b60SAndreas Gohr    protected function debug($msg, $file, $line)
260da369b60SAndreas Gohr    {
26185916a2dSAndreas Gohr        msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
2621078ec26SAndreas Gohr    }
2631078ec26SAndreas Gohr}
264