xref: /plugin/pureldap/classes/Client.php (revision bf69b89c042d8a8a10fdd1dd78cc14b8f933bfe1)
1<?php
2
3namespace dokuwiki\plugin\pureldap\classes;
4
5use dokuwiki\Utf8\PhpString;
6use FreeDSx\Ldap\Entry\Attribute;
7use FreeDSx\Ldap\Exception\BindException;
8use FreeDSx\Ldap\Exception\ConnectionException;
9use FreeDSx\Ldap\Exception\OperationException;
10use FreeDSx\Ldap\LdapClient;
11
12require_once __DIR__ . '/../vendor/autoload.php';
13
14abstract class Client
15{
16    const FILTER_EQUAL = 'equal';
17    const FILTER_CONTAINS = 'contains';
18    const FILTER_STARTSWITH = 'startsWith';
19    const FILTER_ENDSWITH = 'endsWith';
20
21    /** @var array the configuration */
22    protected $config;
23
24    /** @var LdapClient */
25    protected $ldap;
26
27    /** @var bool is this client authenticated already? */
28    protected $isAuthenticated = false;
29
30    /** @var array cached user info */
31    protected $userCache = [];
32
33    /** @var array cached group list */
34    protected $groupCache = [];
35
36    /**
37     * Client constructor.
38     * @param array $config
39     */
40    public function __construct($config)
41    {
42        $this->config = $this->prepareConfig($config);
43        $this->prepareSSO();
44        $this->ldap = new LdapClient($this->config);
45    }
46
47    /**
48     * Setup sane config defaults
49     *
50     * @param array $config
51     * @return array
52     */
53    protected function prepareConfig($config)
54    {
55        // ensure we have the default keys
56        /** @var array $conf */
57        include __DIR__ . '/../conf/default.php';
58        $defaults = $conf;
59        $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf
60
61        $config = array_merge($defaults, $config);
62
63        // default port depends on SSL setting
64        if (!$config['port']) {
65            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
66        }
67
68        // set ssl parameters
69        $config['use_ssl'] = ($config['encryption'] === 'ssl');
70        if ($config['validate'] === 'none') {
71            $config['ssl_validate_cert'] = false;
72        } elseif ($config['validate'] === 'self') {
73            $config['ssl_allow_self_signed'] = true;
74        }
75
76        $config['suffix'] = PhpString::strtolower($config['suffix']);
77        $config['primarygroup'] = $this->cleanGroup($config['primarygroup']);
78
79        return $config;
80    }
81
82    /**
83     * Extract user info from environment for SSO
84     */
85    protected function prepareSSO()
86    {
87        global $INPUT;
88
89        if (!$this->config['sso']) return;
90        if ($INPUT->server->str('REMOTE_USER') === '') return;
91
92        $user = $INPUT->server->str('REMOTE_USER');
93
94        // make sure the right encoding is used
95        if ($this->config['sso_charset']) {
96            $user = iconv($this->config['sso_charset'], 'UTF-8', $user);
97        } elseif (!\dokuwiki\Utf8\Clean::isUtf8($user)) {
98            $user = utf8_encode($user);
99        }
100        $user = $this->cleanUser($user);
101
102        // Prepare SSO
103
104        // trust the incoming user
105        $INPUT->server->set('REMOTE_USER', $user);
106
107        // we need to simulate a login
108        if (empty($_COOKIE[DOKU_COOKIE])) {
109            $INPUT->set('u', $user);
110            $INPUT->set('p', 'sso_only');
111        }
112    }
113
114    /**
115     * Authenticate as admin
116     */
117    public function autoAuth()
118    {
119        if ($this->isAuthenticated) return true;
120
121        $user = $this->prepareBindUser($this->config['admin_username']);
122        $ok = $this->authenticate($user, $this->config['admin_password']);
123        if (!$ok) {
124            $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
125        }
126        return $ok;
127    }
128
129    /**
130     * Authenticates a given user. This client will remain authenticated
131     *
132     * @param string $user
133     * @param string $pass
134     * @return bool was the authentication successful?
135     * @noinspection PhpRedundantCatchClauseInspection
136     */
137    public function authenticate($user, $pass)
138    {
139        $user = $this->prepareBindUser($user);
140
141        if ($this->config['encryption'] === 'tls') {
142            try {
143                $this->ldap->startTls();
144            } catch (OperationException $e) {
145                $this->fatal($e);
146            }
147        }
148
149        try {
150            $this->ldap->bind($user, $pass);
151        } catch (BindException $e) {
152            return false;
153        } catch (ConnectionException $e) {
154            $this->fatal($e);
155            return false;
156        } catch (OperationException $e) {
157            $this->fatal($e);
158            return false;
159        }
160
161        $this->isAuthenticated = true;
162        return true;
163    }
164
165    /**
166     * Get info for a single user, use cache if available
167     *
168     * @param string $username
169     * @param bool $fetchgroups Are groups needed?
170     * @return array|null
171     */
172    public function getCachedUser($username, $fetchgroups = true)
173    {
174        global $conf;
175
176        // memory cache first
177        if (isset($this->userCache[$username])) {
178            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
179                return $this->userCache[$username];
180            }
181        }
182
183        // disk cache second
184        $cachename = getCacheName($username, '.pureldap-user');
185        $cachetime = @filemtime($cachename);
186        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
187            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
188            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
189                return $this->userCache[$username];
190            }
191        }
192
193        // fetch fresh data
194        $info = $this->getUser($username, $fetchgroups);
195
196        // store in cache
197        if ($info !== null) {
198            $this->userCache[$username] = $info;
199            file_put_contents($cachename, json_encode($info));
200        }
201
202        return $info;
203    }
204
205    /**
206     * Fetch a single user
207     *
208     * @param string $username
209     * @param bool $fetchgroups Shall groups be fetched, too?
210     * @return null|array
211     */
212    abstract public function getUser($username, $fetchgroups = true);
213
214    /**
215     * Return a list of all available groups, use cache if available
216     *
217     * @return string[]
218     */
219    public function getCachedGroups()
220    {
221        if (empty($this->groupCache)) {
222            $this->groupCache = $this->getGroups();
223        }
224
225        return $this->groupCache;
226    }
227
228    /**
229     * Return a list of all available groups
230     *
231     * Optionally filter the list
232     *
233     * @param null|string $match Filter for this, null for all groups
234     * @param string $filtermethod How to match the groups
235     * @return string[]
236     */
237    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
238
239    /**
240     * Clean the user name for use in DokuWiki
241     *
242     * @param string $user
243     * @return string
244     */
245    abstract public function cleanUser($user);
246
247    /**
248     * Clean the group name for use in DokuWiki
249     *
250     * @param string $group
251     * @return string
252     */
253    abstract public function cleanGroup($group);
254
255    /**
256     * Inheriting classes may want to manipulate the user before binding
257     *
258     * @param string $user
259     * @return string
260     */
261    protected function prepareBindUser($user)
262    {
263        return $user;
264    }
265
266    /**
267     * Helper method to get the first value of the given attribute
268     *
269     * The given attribute may be null, an empty string is returned then
270     *
271     * @param Attribute|null $attribute
272     * @return string
273     */
274    protected function attr2str($attribute)
275    {
276        if ($attribute !== null) {
277            return $attribute->firstValue();
278        }
279        return '';
280    }
281
282    /**
283     * Get the attributes that should be fetched for a user
284     *
285     * Can be extended in sub classes
286     *
287     * @return Attribute[]
288     */
289    protected function userAttributes()
290    {
291        // defaults
292        $attr = [
293            new Attribute('dn'),
294            new Attribute('displayName'),
295            new Attribute('mail'),
296        ];
297        // additionals
298        foreach ($this->config['attributes'] as $attribute) {
299            $attr[] = new Attribute($attribute);
300        }
301        return $attr;
302    }
303
304    /**
305     * Handle fatal exceptions
306     *
307     * @param \Exception $e
308     */
309    protected function fatal(\Exception $e)
310    {
311        if (class_exists('\dokuwiki\ErrorHandler')) {
312            \dokuwiki\ErrorHandler::logException($e);
313        }
314
315        if (defined('DOKU_UNITTEST')) {
316            throw new \RuntimeException('', 0, $e);
317        }
318
319        msg('[pureldap] ' . hsc($e->getMessage()), -1);
320    }
321
322    /**
323     * Handle error output
324     *
325     * @param string $msg
326     * @param string $file
327     * @param int $line
328     */
329    protected function error($msg, $file, $line)
330    {
331        if (class_exists('\dokuwiki\Logger')) {
332            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
333        }
334
335        if (defined('DOKU_UNITTEST')) {
336            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
337        }
338
339        msg('[pureldap] ' . hsc($msg), -1);
340    }
341
342    /**
343     * Handle debug output
344     *
345     * @param string $msg
346     * @param string $file
347     * @param int $line
348     */
349    protected function debug($msg, $file, $line)
350    {
351        global $conf;
352
353        if (class_exists('\dokuwiki\Logger')) {
354            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
355        }
356
357        if ($conf['allowdebug']) {
358            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
359        }
360    }
361}
362