xref: /plugin/pureldap/classes/ADClient.php (revision 9c590892fd92272f32265b97584f4f97be2fffc7)
1<?php
2
3namespace dokuwiki\plugin\pureldap\classes;
4
5use dokuwiki\Utf8\PhpString;
6use FreeDSx\Ldap\Entry\Attribute;
7use FreeDSx\Ldap\Entry\Entries;
8use FreeDSx\Ldap\Entry\Entry;
9use FreeDSx\Ldap\Exception\OperationException;
10use FreeDSx\Ldap\Exception\ProtocolException;
11use FreeDSx\Ldap\Operations;
12use FreeDSx\Ldap\Search\Filters;
13
14class ADClient extends Client
15{
16
17    /** @inheritDoc */
18    public function getUser($username, $fetchgroups = true)
19    {
20        if (!$this->autoAuth()) return null;
21
22        $filter = Filters::and(
23            Filters::equal('objectClass', 'user'),
24            Filters::equal('userPrincipalName', $this->qualifiedUser($username))
25        );
26        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
27
28        try {
29            /** @var Entries $entries */
30            $attributes = $this->userAttributes();
31            $entries = $this->ldap->search(Operations::search($filter, ...$attributes));
32        } catch (OperationException $e) {
33            $this->fatal($e);
34            return null;
35        }
36        if ($entries->count() !== 1) return null;
37        $entry = $entries->first();
38        return $this->entry2User($entry);
39    }
40
41    /** @inheritDoc */
42    public function getGroups($match = null, $filtermethod = 'equal')
43    {
44        if (!$this->autoAuth()) return [];
45
46        $filter = Filters::and(
47            Filters::equal('objectClass', 'group')
48        );
49        if ($match !== null) {
50            // FIXME this is a workaround that removes regex anchors as passed by the groupuser plugin
51            // a proper fix requires splitbrain/dokuwiki#3028 to be properly fixed
52            $match = ltrim($match, '^');
53            $match = rtrim($match, '$');
54
55            $filter->add(Filters::$filtermethod('cn', $match));
56        }
57
58        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
59        $search = Operations::search($filter, 'cn');
60        $paging = $this->ldap->paging($search);
61
62        $groups = [];
63        while ($paging->hasEntries()) {
64            try {
65                $entries = $paging->getEntries();
66            } catch (ProtocolException $e) {
67                $this->fatal($e);
68                return $groups; // we return what we got so far
69            }
70
71            foreach ($entries as $entry) {
72                /** @var Entry $entry */
73                $groups[$entry->getDn()->toString()] = $this->attr2str($entry->get('cn'));
74            }
75        }
76
77        asort($groups);
78        return $groups;
79    }
80
81    /**
82     * Fetch users matching the given filters
83     *
84     * @param array $match
85     * @param string $filtermethod The method to use for filtering
86     * @return array
87     */
88    public function getFilteredUsers($match, $filtermethod = 'equal')
89    {
90        if (!$this->autoAuth()) return [];
91
92        $filter = Filters::and(Filters::equal('objectClass', 'user'));
93        if (isset($match['user'])) {
94            $filter->add(Filters::$filtermethod('userPrincipalName', $match['user']));
95        }
96        if (isset($match['name'])) {
97            $filter->add(Filters::$filtermethod('displayName', $match['name']));
98        }
99        if (isset($match['mail'])) {
100            $filter->add(Filters::$filtermethod('mail', $match['mail']));
101        }
102        if (isset($match['grps'])) {
103            // memberOf can not be checked with a substring match, so we need to get the right groups first
104            $groups = $this->getGroups($match['grps'], $filtermethod);
105            $or = Filters::or();
106            foreach ($groups as $dn => $group) {
107                $or->add(Filters::equal('memberOf', $dn));
108            }
109            $filter->add($or);
110        }
111        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
112        $attributes = $this->userAttributes();
113        $search = Operations::search($filter, ...$attributes);
114        $paging = $this->ldap->paging($search);
115
116        $users = [];
117        while ($paging->hasEntries()) {
118            try {
119                $entries = $paging->getEntries();
120            } catch (ProtocolException $e) {
121                $this->fatal($e);
122                break; // we abort and return what we have so far
123            }
124
125            foreach ($entries as $entry) {
126                $userinfo = $this->entry2User($entry);
127                $users[$userinfo['user']] = $this->entry2User($entry);
128            }
129        }
130
131        ksort($users);
132        return $users;
133    }
134
135    /**
136     * @inheritDoc
137     * userPrincipalName in the form <user>@<domain>
138     */
139    public function qualifiedUser($user)
140    {
141        $user = PhpString::strtolower($user);
142        if (!$this->config['domain']) return $user;
143
144        list($user, $domain) = explode('@', $user, 2);
145        if (!$domain) {
146            $domain = $this->config['domain'];
147        }
148
149        return $user . '@' . $domain;
150    }
151
152    /**
153     * @inheritDoc
154     * Removes the account suffix from the given user
155     */
156    public function simpleUser($user)
157    {
158        $user = PhpString::strtolower($user);
159        if (!$this->config['domain']) return $user;
160
161        // strip account suffix
162        list($luser, $suffix) = explode('@', $user, 2);
163        if ($suffix === $this->config['domain']) return $luser;
164
165        return $user;
166    }
167
168    /**
169     * Transform an LDAP entry to a user info array
170     *
171     * @param Entry $entry
172     * @return array
173     */
174    protected function entry2User(Entry $entry)
175    {
176        $user = [
177            'user' => $this->simpleUser($this->attr2str($entry->get('UserPrincipalName'))),
178            'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')),
179            'mail' => $this->attr2str($entry->get('mail')),
180            'dn' => $entry->getDn()->toString(),
181            'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive
182        ];
183
184        // get additional attributes
185        foreach ($this->config['attributes'] as $attr) {
186            $user[$attr] = $this->attr2str($entry->get($attr));
187        }
188
189        return $user;
190    }
191
192    /**
193     * Get the list of groups the given user is member of
194     *
195     * This method currently does no LDAP queries and thus is inexpensive.
196     *
197     * @param Entry $userentry
198     * @return array
199     * @todo implement nested group memberships FIXME already correct?
200     */
201    protected function getUserGroups(Entry $userentry)
202    {
203        $groups = [$this->config['defaultgroup']]; // always add default
204
205        // we simply take the first CN= part of the group DN and return it as the group name
206        // this should be correct for ActiveDirectory and saves us additional LDAP queries
207        if ($userentry->has('memberOf')) {
208            foreach ($userentry->get('memberOf')->getValues() as $dn) {
209                list($cn) = explode(',', $dn, 2);
210                $groups[] = PhpString::strtolower(substr($cn, 3));
211            }
212        }
213
214        // resolving the primary group in AD is complicated but basically never needed
215        // http://support.microsoft.com/?kbid=321360
216        $gid = $userentry->get('primaryGroupID')->firstValue();
217        if ($gid == 513) {
218            $groups[] = 'domain users';
219        }
220
221        sort($groups);
222        return $groups;
223    }
224
225    /** @inheritDoc */
226    protected function userAttributes()
227    {
228        $attr = parent::userAttributes();
229        $attr[] = new Attribute('UserPrincipalName');
230        $attr[] = new Attribute('Name');
231        $attr[] = new Attribute('primaryGroupID');
232        $attr[] = new Attribute('memberOf');
233
234        return $attr;
235    }
236
237}
238