1<?php
2/**
3 * ActiveDirectoryCommonirectoryCommon.php
4 *
5 * Common code from AD auth modules
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 *
20 * @link       https://www.librenms.org
21 * @copyright  2018 Tony Murray
22 * @author     Tony Murray <murraytony@gmail.com>
23 */
24
25namespace LibreNMS\Authentication;
26
27use LibreNMS\Config;
28
29trait ActiveDirectoryCommon
30{
31    protected function getUseridFromSid($sid)
32    {
33        return preg_replace('/.*-(\d+)$/', '$1', $sid);
34    }
35
36    protected function sidFromLdap($sid)
37    {
38        $sidUnpacked = unpack('H*hex', $sid);
39        $sidHex = array_shift($sidUnpacked);
40        $subAuths = unpack('H2/H2/n/N/V*', $sid);
41        if (PHP_INT_SIZE <= 4) {
42            for ($i = 1; $i <= count($subAuths); $i++) {
43                if ($subAuths[$i] < 0) {
44                    $subAuths[$i] = $subAuths[$i] + 0x100000000;
45                }
46            }
47        }
48        $revLevel = hexdec(substr($sidHex, 0, 2));
49        $authIdent = hexdec(substr($sidHex, 4, 12));
50
51        return 'S-' . $revLevel . '-' . $authIdent . '-' . implode('-', $subAuths);
52    }
53
54    protected function getCn($dn)
55    {
56        $dn = str_replace('\\,', '~C0mmA~', $dn);
57        preg_match('/[^,]*/', $dn, $matches, PREG_OFFSET_CAPTURE, 3);
58
59        return str_replace('~C0mmA~', ',', $matches[0][0]);
60    }
61
62    protected function getDn($samaccountname)
63    {
64        $link_identifier = $this->getConnection();
65        $attributes = ['dn'];
66        $result = ldap_search(
67            $link_identifier,
68            Config::get('auth_ad_base_dn'),
69            $this->groupFilter($samaccountname),
70            $attributes
71        );
72        $entries = ldap_get_entries($link_identifier, $result);
73        if ($entries['count'] > 0) {
74            return $entries[0]['dn'];
75        } else {
76            return '';
77        }
78    }
79
80    protected function userFilter($username)
81    {
82        // don't return disabled users
83        $user_filter = "(&(samaccountname=$username)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2))";
84
85        $extra = Config::get('auth_ad_user_filter');
86        if ($extra) {
87            $user_filter .= $extra;
88        }
89        $user_filter .= ')';
90
91        return $user_filter;
92    }
93
94    protected function groupFilter($groupname)
95    {
96        $group_filter = "(samaccountname=$groupname)";
97
98        $extra = Config::get('auth_ad_group_filter');
99        if ($extra) {
100            $group_filter = "(&$extra$group_filter)";
101        }
102
103        return $group_filter;
104    }
105
106    protected function getFullname($username)
107    {
108        $connection = $this->getConnection();
109        $attributes = ['name'];
110        $result = ldap_search(
111            $connection,
112            Config::get('auth_ad_base_dn'),
113            $this->userFilter($username),
114            $attributes
115        );
116        $entries = ldap_get_entries($connection, $result);
117        if ($entries['count'] > 0) {
118            $membername = $entries[0]['name'][0];
119        } else {
120            $membername = $username;
121        }
122
123        return $membername;
124    }
125
126    public function getGroupList()
127    {
128        $ldap_groups = [];
129
130        // show all Active Directory Users by default
131        $default_group = 'Users';
132
133        if (Config::has('auth_ad_group')) {
134            if (Config::get('auth_ad_group') !== $default_group) {
135                $ldap_groups[] = Config::get('auth_ad_group');
136            }
137        }
138
139        if (! Config::has('auth_ad_groups') && ! Config::has('auth_ad_group')) {
140            $ldap_groups[] = $this->getDn($default_group);
141        }
142
143        foreach (Config::get('auth_ad_groups') as $key => $value) {
144            $ldap_groups[] = $this->getDn($key);
145        }
146
147        return $ldap_groups;
148    }
149
150    public function getUserlist()
151    {
152        $connection = $this->getConnection();
153
154        $userlist = [];
155        $ldap_groups = $this->getGroupList();
156
157        foreach ($ldap_groups as $ldap_group) {
158            $search_filter = "(&(memberOf:1.2.840.113556.1.4.1941:=$ldap_group)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
159            if (Config::get('auth_ad_user_filter')) {
160                $search_filter = '(&' . Config::get('auth_ad_user_filter') . $search_filter . ')';
161            }
162            $attributes = ['samaccountname', 'displayname', 'objectsid', 'mail'];
163            $search = ldap_search($connection, Config::get('auth_ad_base_dn'), $search_filter, $attributes);
164            $results = ldap_get_entries($connection, $search);
165
166            foreach ($results as $result) {
167                if (isset($result['samaccountname'][0])) {
168                    $userlist[$result['samaccountname'][0]] = $this->userFromAd($result);
169                }
170            }
171        }
172
173        return array_values($userlist);
174    }
175
176    /**
177     * Generate a user array from an AD LDAP entry
178     * Must have the attributes: objectsid, samaccountname, displayname, mail
179     * @internal
180     *
181     * @param array $entry
182     * @return array
183     */
184    protected function userFromAd($entry)
185    {
186        return [
187            'user_id' => $this->getUseridFromSid($this->sidFromLdap($entry['objectsid'][0])),
188            'username' => $entry['samaccountname'][0],
189            'realname' => $entry['displayname'][0],
190            'email' => isset($entry['mail'][0]) ? $entry['mail'][0] : null,
191            'descr' => '',
192            'level' => $this->getUserlevel($entry['samaccountname'][0]),
193            'can_modify_passwd' => 0,
194        ];
195    }
196
197    public function getUser($user_id)
198    {
199        $connection = $this->getConnection();
200        $domain_sid = $this->getDomainSid();
201
202        $search_filter = "(&(objectcategory=person)(objectclass=user)(objectsid=$domain_sid-$user_id))";
203        $attributes = ['samaccountname', 'displayname', 'objectsid', 'mail'];
204        $search = ldap_search($connection, Config::get('auth_ad_base_dn'), $search_filter, $attributes);
205        $entry = ldap_get_entries($connection, $search);
206
207        if (isset($entry[0]['samaccountname'][0])) {
208            return $this->userFromAd($entry[0]);
209        }
210
211        return [];
212    }
213
214    protected function getDomainSid()
215    {
216        $connection = $this->getConnection();
217
218        // Extract only the domain components
219        $dn_candidate = preg_replace('/^.*?DC=/i', 'DC=', Config::get('auth_ad_base_dn'));
220
221        $search = ldap_read(
222            $connection,
223            $dn_candidate,
224            '(objectClass=*)',
225            ['objectsid']
226        );
227        $entry = ldap_get_entries($connection, $search);
228
229        return substr($this->sidFromLdap($entry[0]['objectsid'][0]), 0, 41);
230    }
231
232    /**
233     * Provide a connected and bound ldap connection resource
234     *
235     * @return resource
236     */
237    abstract protected function getConnection();
238}
239