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