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