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