xref: /plugin/pureldap/classes/ADClient.php (revision 9c590892fd92272f32265b97584f4f97be2fffc7)
11078ec26SAndreas Gohr<?php
21078ec26SAndreas Gohr
31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes;
41078ec26SAndreas Gohr
580ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString;
6*9c590892SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute;
71078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Entries;
81078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Entry;
91078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException;
105a3b9122SAndreas Gohruse FreeDSx\Ldap\Exception\ProtocolException;
111078ec26SAndreas Gohruse FreeDSx\Ldap\Operations;
121078ec26SAndreas Gohruse FreeDSx\Ldap\Search\Filters;
131078ec26SAndreas Gohr
141078ec26SAndreas Gohrclass ADClient extends Client
151078ec26SAndreas Gohr{
161078ec26SAndreas Gohr
171078ec26SAndreas Gohr    /** @inheritDoc */
181078ec26SAndreas Gohr    public function getUser($username, $fetchgroups = true)
191078ec26SAndreas Gohr    {
201078ec26SAndreas Gohr        if (!$this->autoAuth()) return null;
211078ec26SAndreas Gohr
221078ec26SAndreas Gohr        $filter = Filters::and(
231078ec26SAndreas Gohr            Filters::equal('objectClass', 'user'),
2480ac552fSAndreas Gohr            Filters::equal('userPrincipalName', $this->qualifiedUser($username))
251078ec26SAndreas Gohr        );
26b21740b4SAndreas Gohr        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
271078ec26SAndreas Gohr
281078ec26SAndreas Gohr        try {
291078ec26SAndreas Gohr            /** @var Entries $entries */
30*9c590892SAndreas Gohr            $attributes = $this->userAttributes();
31*9c590892SAndreas Gohr            $entries = $this->ldap->search(Operations::search($filter, ...$attributes));
321078ec26SAndreas Gohr        } catch (OperationException $e) {
33b21740b4SAndreas Gohr            $this->fatal($e);
341078ec26SAndreas Gohr            return null;
351078ec26SAndreas Gohr        }
361078ec26SAndreas Gohr        if ($entries->count() !== 1) return null;
371078ec26SAndreas Gohr        $entry = $entries->first();
38b21740b4SAndreas Gohr        return $this->entry2User($entry);
39b21740b4SAndreas Gohr    }
401078ec26SAndreas Gohr
41b21740b4SAndreas Gohr    /** @inheritDoc */
42b21740b4SAndreas Gohr    public function getGroups($match = null, $filtermethod = 'equal')
43b21740b4SAndreas Gohr    {
44b21740b4SAndreas Gohr        if (!$this->autoAuth()) return [];
45b21740b4SAndreas Gohr
46b21740b4SAndreas Gohr        $filter = Filters::and(
47b21740b4SAndreas Gohr            Filters::equal('objectClass', 'group')
48b21740b4SAndreas Gohr        );
49b21740b4SAndreas Gohr        if ($match !== null) {
50fce018daSAndreas Gohr            // FIXME this is a workaround that removes regex anchors as passed by the groupuser plugin
51fce018daSAndreas Gohr            // a proper fix requires splitbrain/dokuwiki#3028 to be properly fixed
52fce018daSAndreas Gohr            $match = ltrim($match, '^');
53fce018daSAndreas Gohr            $match = rtrim($match, '$');
54fce018daSAndreas Gohr
55b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('cn', $match));
56b21740b4SAndreas Gohr        }
57b21740b4SAndreas Gohr
58b21740b4SAndreas Gohr        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
59b21740b4SAndreas Gohr        $search = Operations::search($filter, 'cn');
60b21740b4SAndreas Gohr        $paging = $this->ldap->paging($search);
61b21740b4SAndreas Gohr
62b21740b4SAndreas Gohr        $groups = [];
63b21740b4SAndreas Gohr        while ($paging->hasEntries()) {
64b21740b4SAndreas Gohr            try {
65b21740b4SAndreas Gohr                $entries = $paging->getEntries();
66b21740b4SAndreas Gohr            } catch (ProtocolException $e) {
67b21740b4SAndreas Gohr                $this->fatal($e);
68b21740b4SAndreas Gohr                return $groups; // we return what we got so far
69b21740b4SAndreas Gohr            }
70b21740b4SAndreas Gohr
71b21740b4SAndreas Gohr            foreach ($entries as $entry) {
72b21740b4SAndreas Gohr                /** @var Entry $entry */
73b21740b4SAndreas Gohr                $groups[$entry->getDn()->toString()] = $this->attr2str($entry->get('cn'));
74b21740b4SAndreas Gohr            }
75b21740b4SAndreas Gohr        }
76b21740b4SAndreas Gohr
771b0eb9b3SAndreas Gohr        asort($groups);
78b21740b4SAndreas Gohr        return $groups;
79b21740b4SAndreas Gohr    }
80b21740b4SAndreas Gohr
81b21740b4SAndreas Gohr    /**
82b21740b4SAndreas Gohr     * Fetch users matching the given filters
83b21740b4SAndreas Gohr     *
84b21740b4SAndreas Gohr     * @param array $match
85b21740b4SAndreas Gohr     * @param string $filtermethod The method to use for filtering
86b21740b4SAndreas Gohr     * @return array
87b21740b4SAndreas Gohr     */
88b21740b4SAndreas Gohr    public function getFilteredUsers($match, $filtermethod = 'equal')
89b21740b4SAndreas Gohr    {
90b21740b4SAndreas Gohr        if (!$this->autoAuth()) return [];
91b21740b4SAndreas Gohr
92b21740b4SAndreas Gohr        $filter = Filters::and(Filters::equal('objectClass', 'user'));
93b21740b4SAndreas Gohr        if (isset($match['user'])) {
94b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('userPrincipalName', $match['user']));
95b21740b4SAndreas Gohr        }
96b21740b4SAndreas Gohr        if (isset($match['name'])) {
97b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('displayName', $match['name']));
98b21740b4SAndreas Gohr        }
99b21740b4SAndreas Gohr        if (isset($match['mail'])) {
100b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('mail', $match['mail']));
101b21740b4SAndreas Gohr        }
102b21740b4SAndreas Gohr        if (isset($match['grps'])) {
103b21740b4SAndreas Gohr            // memberOf can not be checked with a substring match, so we need to get the right groups first
104b21740b4SAndreas Gohr            $groups = $this->getGroups($match['grps'], $filtermethod);
105b21740b4SAndreas Gohr            $or = Filters::or();
106b21740b4SAndreas Gohr            foreach ($groups as $dn => $group) {
107b21740b4SAndreas Gohr                $or->add(Filters::equal('memberOf', $dn));
108b21740b4SAndreas Gohr            }
109b21740b4SAndreas Gohr            $filter->add($or);
110b21740b4SAndreas Gohr        }
111b21740b4SAndreas Gohr        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
112*9c590892SAndreas Gohr        $attributes = $this->userAttributes();
113*9c590892SAndreas Gohr        $search = Operations::search($filter, ...$attributes);
114b21740b4SAndreas Gohr        $paging = $this->ldap->paging($search);
115b21740b4SAndreas Gohr
116b21740b4SAndreas Gohr        $users = [];
117b21740b4SAndreas Gohr        while ($paging->hasEntries()) {
118b21740b4SAndreas Gohr            try {
119b21740b4SAndreas Gohr                $entries = $paging->getEntries();
120b21740b4SAndreas Gohr            } catch (ProtocolException $e) {
121b21740b4SAndreas Gohr                $this->fatal($e);
12280ac552fSAndreas Gohr                break; // we abort and return what we have so far
123b21740b4SAndreas Gohr            }
124b21740b4SAndreas Gohr
125b21740b4SAndreas Gohr            foreach ($entries as $entry) {
12680ac552fSAndreas Gohr                $userinfo = $this->entry2User($entry);
12780ac552fSAndreas Gohr                $users[$userinfo['user']] = $this->entry2User($entry);
128b21740b4SAndreas Gohr            }
129b21740b4SAndreas Gohr        }
130b21740b4SAndreas Gohr
1311b0eb9b3SAndreas Gohr        ksort($users);
132b21740b4SAndreas Gohr        return $users;
133b21740b4SAndreas Gohr    }
134b21740b4SAndreas Gohr
135b21740b4SAndreas Gohr    /**
13680ac552fSAndreas Gohr     * @inheritDoc
13780ac552fSAndreas Gohr     * userPrincipalName in the form <user>@<domain>
13880ac552fSAndreas Gohr     */
13980ac552fSAndreas Gohr    public function qualifiedUser($user)
14080ac552fSAndreas Gohr    {
14180ac552fSAndreas Gohr        $user = PhpString::strtolower($user);
14280ac552fSAndreas Gohr        if (!$this->config['domain']) return $user;
14380ac552fSAndreas Gohr
14480ac552fSAndreas Gohr        list($user, $domain) = explode('@', $user, 2);
14580ac552fSAndreas Gohr        if (!$domain) {
14680ac552fSAndreas Gohr            $domain = $this->config['domain'];
14780ac552fSAndreas Gohr        }
14880ac552fSAndreas Gohr
14980ac552fSAndreas Gohr        return $user . '@' . $domain;
15080ac552fSAndreas Gohr    }
15180ac552fSAndreas Gohr
15280ac552fSAndreas Gohr    /**
15380ac552fSAndreas Gohr     * @inheritDoc
15480ac552fSAndreas Gohr     * Removes the account suffix from the given user
15580ac552fSAndreas Gohr     */
15680ac552fSAndreas Gohr    public function simpleUser($user)
15780ac552fSAndreas Gohr    {
15880ac552fSAndreas Gohr        $user = PhpString::strtolower($user);
15980ac552fSAndreas Gohr        if (!$this->config['domain']) return $user;
16080ac552fSAndreas Gohr
16180ac552fSAndreas Gohr        // strip account suffix
16280ac552fSAndreas Gohr        list($luser, $suffix) = explode('@', $user, 2);
16380ac552fSAndreas Gohr        if ($suffix === $this->config['domain']) return $luser;
16480ac552fSAndreas Gohr
16580ac552fSAndreas Gohr        return $user;
16680ac552fSAndreas Gohr    }
16780ac552fSAndreas Gohr
16880ac552fSAndreas Gohr    /**
169b21740b4SAndreas Gohr     * Transform an LDAP entry to a user info array
170b21740b4SAndreas Gohr     *
171b21740b4SAndreas Gohr     * @param Entry $entry
172b21740b4SAndreas Gohr     * @return array
173b21740b4SAndreas Gohr     */
174b21740b4SAndreas Gohr    protected function entry2User(Entry $entry)
175b21740b4SAndreas Gohr    {
176b914569fSAndreas Gohr        $user = [
17780ac552fSAndreas Gohr            'user' => $this->simpleUser($this->attr2str($entry->get('UserPrincipalName'))),
1781078ec26SAndreas Gohr            'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')),
1791078ec26SAndreas Gohr            'mail' => $this->attr2str($entry->get('mail')),
1801078ec26SAndreas Gohr            'dn' => $entry->getDn()->toString(),
1811078ec26SAndreas Gohr            'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive
1821078ec26SAndreas Gohr        ];
183b914569fSAndreas Gohr
184b914569fSAndreas Gohr        // get additional attributes
185b914569fSAndreas Gohr        foreach ($this->config['attributes'] as $attr) {
186b914569fSAndreas Gohr            $user[$attr] = $this->attr2str($entry->get($attr));
187b914569fSAndreas Gohr        }
188b914569fSAndreas Gohr
189b914569fSAndreas Gohr        return $user;
1901078ec26SAndreas Gohr    }
1911078ec26SAndreas Gohr
1921078ec26SAndreas Gohr    /**
1931078ec26SAndreas Gohr     * Get the list of groups the given user is member of
1941078ec26SAndreas Gohr     *
1951078ec26SAndreas Gohr     * This method currently does no LDAP queries and thus is inexpensive.
1961078ec26SAndreas Gohr     *
1971078ec26SAndreas Gohr     * @param Entry $userentry
1981078ec26SAndreas Gohr     * @return array
1996d90d5c8SAndreas Gohr     * @todo implement nested group memberships FIXME already correct?
2001078ec26SAndreas Gohr     */
2011078ec26SAndreas Gohr    protected function getUserGroups(Entry $userentry)
2021078ec26SAndreas Gohr    {
2031078ec26SAndreas Gohr        $groups = [$this->config['defaultgroup']]; // always add default
2041078ec26SAndreas Gohr
2051078ec26SAndreas Gohr        // we simply take the first CN= part of the group DN and return it as the group name
2061078ec26SAndreas Gohr        // this should be correct for ActiveDirectory and saves us additional LDAP queries
2071078ec26SAndreas Gohr        if ($userentry->has('memberOf')) {
208b21740b4SAndreas Gohr            foreach ($userentry->get('memberOf')->getValues() as $dn) {
209b21740b4SAndreas Gohr                list($cn) = explode(',', $dn, 2);
21080ac552fSAndreas Gohr                $groups[] = PhpString::strtolower(substr($cn, 3));
2111078ec26SAndreas Gohr            }
2121078ec26SAndreas Gohr        }
2131078ec26SAndreas Gohr
2141078ec26SAndreas Gohr        // resolving the primary group in AD is complicated but basically never needed
2151078ec26SAndreas Gohr        // http://support.microsoft.com/?kbid=321360
2161078ec26SAndreas Gohr        $gid = $userentry->get('primaryGroupID')->firstValue();
2171078ec26SAndreas Gohr        if ($gid == 513) {
21880ac552fSAndreas Gohr            $groups[] = 'domain users';
2191078ec26SAndreas Gohr        }
2201078ec26SAndreas Gohr
2211b0eb9b3SAndreas Gohr        sort($groups);
2221078ec26SAndreas Gohr        return $groups;
2231078ec26SAndreas Gohr    }
224*9c590892SAndreas Gohr
225*9c590892SAndreas Gohr    /** @inheritDoc */
226*9c590892SAndreas Gohr    protected function userAttributes()
227*9c590892SAndreas Gohr    {
228*9c590892SAndreas Gohr        $attr = parent::userAttributes();
229*9c590892SAndreas Gohr        $attr[] = new Attribute('UserPrincipalName');
230*9c590892SAndreas Gohr        $attr[] = new Attribute('Name');
231*9c590892SAndreas Gohr        $attr[] = new Attribute('primaryGroupID');
232*9c590892SAndreas Gohr        $attr[] = new Attribute('memberOf');
233*9c590892SAndreas Gohr
234*9c590892SAndreas Gohr        return $attr;
235*9c590892SAndreas Gohr    }
236*9c590892SAndreas Gohr
2371078ec26SAndreas Gohr}
238