xref: /plugin/pureldap/classes/Client.php (revision f17bb68b5a2d095b69b9e951aa10c6b366b7a7ce)
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     * Access to the config values
116     *
117     * Because the client class does configuration cleanup, the auth class should access
118     * config values through the client
119     *
120     * @param string $key
121     * @return mixed returns null on missing config
122     */
123    public function getConf($key) {
124        if(!isset($this->config[$key])) return null;
125        return $this->config[$key];
126    }
127
128    /**
129     * Authenticate as admin
130     */
131    public function autoAuth()
132    {
133        if ($this->isAuthenticated) return true;
134
135        $user = $this->prepareBindUser($this->config['admin_username']);
136        $ok = $this->authenticate($user, $this->config['admin_password']);
137        if (!$ok) {
138            $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
139        }
140        return $ok;
141    }
142
143    /**
144     * Authenticates a given user. This client will remain authenticated
145     *
146     * @param string $user
147     * @param string $pass
148     * @return bool was the authentication successful?
149     * @noinspection PhpRedundantCatchClauseInspection
150     */
151    public function authenticate($user, $pass)
152    {
153        $user = $this->prepareBindUser($user);
154
155        if ($this->config['encryption'] === 'tls') {
156            try {
157                $this->ldap->startTls();
158            } catch (OperationException $e) {
159                $this->fatal($e);
160            }
161        }
162
163        try {
164            $this->ldap->bind($user, $pass);
165        } catch (BindException $e) {
166            return false;
167        } catch (ConnectionException $e) {
168            $this->fatal($e);
169            return false;
170        } catch (OperationException $e) {
171            $this->fatal($e);
172            return false;
173        }
174
175        $this->isAuthenticated = true;
176        return true;
177    }
178
179    /**
180     * Get info for a single user, use cache if available
181     *
182     * @param string $username
183     * @param bool $fetchgroups Are groups needed?
184     * @return array|null
185     */
186    public function getCachedUser($username, $fetchgroups = true)
187    {
188        global $conf;
189
190        // memory cache first
191        if (isset($this->userCache[$username])) {
192            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
193                return $this->userCache[$username];
194            }
195        }
196
197        // disk cache second
198        $cachename = getCacheName($username, '.pureldap-user');
199        $cachetime = @filemtime($cachename);
200        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
201            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
202            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
203                return $this->userCache[$username];
204            }
205        }
206
207        // fetch fresh data
208        $info = $this->getUser($username, $fetchgroups);
209
210        // store in cache
211        if ($info !== null) {
212            $this->userCache[$username] = $info;
213            file_put_contents($cachename, json_encode($info));
214        }
215
216        return $info;
217    }
218
219    /**
220     * Fetch a single user
221     *
222     * @param string $username
223     * @param bool $fetchgroups Shall groups be fetched, too?
224     * @return null|array
225     */
226    abstract public function getUser($username, $fetchgroups = true);
227
228    /**
229     * Return a list of all available groups, use cache if available
230     *
231     * @return string[]
232     */
233    public function getCachedGroups()
234    {
235        if (empty($this->groupCache)) {
236            $this->groupCache = $this->getGroups();
237        }
238
239        return $this->groupCache;
240    }
241
242    /**
243     * Return a list of all available groups
244     *
245     * Optionally filter the list
246     *
247     * @param null|string $match Filter for this, null for all groups
248     * @param string $filtermethod How to match the groups
249     * @return string[]
250     */
251    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
252
253    /**
254     * Clean the user name for use in DokuWiki
255     *
256     * @param string $user
257     * @return string
258     */
259    abstract public function cleanUser($user);
260
261    /**
262     * Clean the group name for use in DokuWiki
263     *
264     * @param string $group
265     * @return string
266     */
267    abstract public function cleanGroup($group);
268
269    /**
270     * Inheriting classes may want to manipulate the user before binding
271     *
272     * @param string $user
273     * @return string
274     */
275    protected function prepareBindUser($user)
276    {
277        return $user;
278    }
279
280    /**
281     * Helper method to get the first value of the given attribute
282     *
283     * The given attribute may be null, an empty string is returned then
284     *
285     * @param Attribute|null $attribute
286     * @return string
287     */
288    protected function attr2str($attribute)
289    {
290        if ($attribute !== null) {
291            return $attribute->firstValue();
292        }
293        return '';
294    }
295
296    /**
297     * Get the attributes that should be fetched for a user
298     *
299     * Can be extended in sub classes
300     *
301     * @return Attribute[]
302     */
303    protected function userAttributes()
304    {
305        // defaults
306        $attr = [
307            new Attribute('dn'),
308            new Attribute('displayName'),
309            new Attribute('mail'),
310        ];
311        // additionals
312        foreach ($this->config['attributes'] as $attribute) {
313            $attr[] = new Attribute($attribute);
314        }
315        return $attr;
316    }
317
318    /**
319     * Handle fatal exceptions
320     *
321     * @param \Exception $e
322     */
323    protected function fatal(\Exception $e)
324    {
325        if (class_exists('\dokuwiki\ErrorHandler')) {
326            \dokuwiki\ErrorHandler::logException($e);
327        }
328
329        if (defined('DOKU_UNITTEST')) {
330            throw new \RuntimeException('', 0, $e);
331        }
332
333        msg('[pureldap] ' . hsc($e->getMessage()), -1);
334    }
335
336    /**
337     * Handle error output
338     *
339     * @param string $msg
340     * @param string $file
341     * @param int $line
342     */
343    protected function error($msg, $file, $line)
344    {
345        if (class_exists('\dokuwiki\Logger')) {
346            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
347        }
348
349        if (defined('DOKU_UNITTEST')) {
350            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
351        }
352
353        msg('[pureldap] ' . hsc($msg), -1);
354    }
355
356    /**
357     * Handle debug output
358     *
359     * @param string $msg
360     * @param string $file
361     * @param int $line
362     */
363    protected function debug($msg, $file, $line)
364    {
365        global $conf;
366
367        if (class_exists('\dokuwiki\Logger')) {
368            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
369        }
370
371        if ($conf['allowdebug']) {
372            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
373        }
374    }
375}
376