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