Passed
Pull Request — master (#28)
by Tim
02:18
created

AttributeAddUsersGroups   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 424
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 33
eloc 166
c 3
b 0
f 0
dl 0
loc 424
rs 9.76

7 Methods

Rating   Name   Duplication   Size   Complexity  
B searchActiveDirectory() 0 75 6
A __construct() 0 8 1
A getGroups() 0 53 5
B search() 0 65 11
A getGroupsActiveDirectory() 0 28 4
A getGroupsOpenLdap() 0 48 3
A process() 0 48 3
1
<?php
2
3
/**
4
 * Does a reverse membership lookup on the logged in user,
5
 * looking for groups it is a member of and adds them to
6
 * a defined attribute, in DN format.
7
 *
8
 * @package SimpleSAMLphp
9
 */
10
11
declare(strict_types=1);
12
13
namespace SimpleSAML\Module\ldap\Auth\Process;
14
15
use SimpleSAML\Assert\Assert;
16
use SimpleSAML\Error;
17
use SimpleSAML\Logger;
18
use SimpleSAML\Module\ldap\Utils\Ldap as LdapUtils;
19
use SimpleSAML\Utils;
20
21
class AttributeAddUsersGroups extends BaseFilter
22
{
23
    /** @var string */
24
    protected string $searchUsername;
25
26
    /** @var string */
27
    protected string $searchPassword;
28
29
    /** @var string */
30
    protected string $product;
31
32
33
    /**
34
     * Initialize this filter.
35
     *
36
     * @param array $config Configuration information about this filter.
37
     * @param mixed $reserved For future use.
38
     */
39
    public function __construct(array $config, $reserved)
40
    {
41
        parent::__construct($config, $reserved);
42
43
        // Get filter specific config options
44
        $this->searchUsername = $this->config->getString('search.username');
45
        $this->searchPassword = $this->config->getString('search.password', null);
46
        $this->product = $this->config->getString('product', 'ActiveDirectory');
47
    }
48
49
50
    /**
51
     * LDAP search filters to be added to the base filters for this authproc-filter.
52
     * It's an array of key => value pairs that will be translated to (key=value) in the ldap query.
53
     *
54
     * @var array
55
     */
56
    protected array $additional_filters;
57
58
59
    /**
60
     * This is run when the filter is processed by SimpleSAML.
61
     * It will attempt to find the current users groups using
62
     * the best method possible for the LDAP product. The groups
63
     * are then added to the request attributes.
64
     *
65
     * @throws \SimpleSAML\Error\Exception
66
     * @param array $request
67
     */
68
    public function process(array &$request): void
69
    {
70
        Assert::keyExists($request, 'Attributes');
71
72
        // Log the process
73
        Logger::debug(
74
            $this->title . 'Attempting to get the users groups...'
75
        );
76
77
        $this->additional_filters = $this->config->getArray('additional_filters', []);
78
79
        // Reference the attributes, just to make the names shorter
80
        $attributes = &$request['Attributes'];
81
        $map = &$this->attribute_map;
82
        $distinguishedName = $attributes[$map['dn']][0];
83
84
        // Get the users groups from LDAP
85
        $groups = $this->getGroups($distinguishedName);
86
87
        // Make the array if it is not set already
88
        if (!isset($attributes[$map['groups']])) {
89
            $attributes[$map['groups']] = [];
90
        }
91
92
        // Must be an array, else cannot merge groups
93
        if (!is_array($attributes[$map['groups']])) {
94
            throw new Error\Exception(
95
                sprintf(
96
                    '%sThe group attribute [%s] is not an array of group DNs. %s',
97
                    $this->title,
98
                     $map['groups'],
99
                    $this->varExport($attributes[$map['groups']])
100
                )
101
            );
102
        }
103
104
        // Add the users group(s)
105
        $group_attribute = &$attributes[$map['groups']];
106
        $group_attribute = array_merge($group_attribute, $groups);
107
        $group_attribute = array_unique($group_attribute);
108
109
        // All done
110
        Logger::debug(
111
            sprintf(
112
                '%sAdded users groups to the group attribute[%s]: %s',
113
                $this->title,
114
                $map['groups'],
115
                implode('; ', $groups)
116
            )
117
        );
118
    }
119
120
121
    /**
122
     * Will perform a search using the required attribute values from the user to
123
     * get their group membership, recursively.
124
     *
125
     * @throws \SimpleSAML\Error\Exception
126
     * @param array $attributes
127
     * @return array
128
     */
129
    protected function getGroups(array $attributes): array
130
    {
131
        // Log the request
132
        Logger::debug(
133
            $this->title . 'Checking for groups based on the best method for the LDAP product.'
134
        );
135
136
        $ldapUtils = new LdapUtils();
137
        $ldap = $ldapUtils->bind($this->ldapServers, $this->searchUsername, $this->searchPassword);
0 ignored issues
show
Unused Code introduced by
The assignment to $ldap is dead and can be removed.
Loading history...
138
139
        // Based on the directory service, search LDAP for groups
140
        // If any attributes are needed, prepare them before calling search method
141
        switch ($this->product) {
142
            case 'ACTIVEDIRECTORY':
143
                $groups = $this->getGroupsActiveDirectory($attributes);
144
                break;
145
            case 'OPENLDAP':
146
                $groups = $this->getGroupsOpenLdap($attributes);
147
                break;
148
            default:
149
                // Reference the map, just to make the name shorter
150
                $map = &$this->attribute_map;
151
152
                // Log the general search
153
                Logger::debug(
154
                    $this->title . 'Searching LDAP using the default search method.'
155
                );
156
157
                // Make sure the defined memberOf attribute exists
158
                if (!isset($attributes[$map['memberof']])) {
159
                    throw new Error\Exception(
160
                        $this->title . 'The memberof attribute [' . $map['memberof'] .
161
                        '] is not defined in the user\'s Attributes: ' . implode(', ', array_keys($attributes))
162
                    );
163
                }
164
165
                // MemberOf must be an array of group DN's
166
                if (!is_array($attributes[$map['memberof']])) {
167
                    throw new Error\Exception(
168
                        $this->title . 'The memberof attribute [' . $map['memberof'] .
169
                        '] is not an array of group DNs. ' . $this->varExport($attributes[$map['memberof']])
170
                    );
171
                }
172
173
                // Search for the users group membership, recursively
174
                $groups = $this->search($attributes[$map['memberof']]);
175
        }
176
177
        // All done
178
        Logger::debug(
179
            $this->title . 'User found to be a member of the groups:' . implode('; ', $groups)
180
        );
181
        return $groups;
182
    }
183
184
185
    /**
186
     * OpenLDAP optimized search
187
     * using the required attribute values from the user to
188
     * get their group membership, recursively.
189
     *
190
     * @throws \SimpleSAML\Error\Exception
191
     * @param array $attributes
192
     * @return array
193
     */
194
    protected function getGroupsOpenLdap(array $attributes): array
195
    {
196
        // Log the OpenLDAP specific search
197
        Logger::debug(
198
            $this->title . 'Searching LDAP using OpenLDAP specific method.'
199
        );
200
201
        // Reference the map, just to make the name shorter
202
        $map = &$this->attribute_map;
203
204
        // Print group search string and search for all group names
205
        $openldap_base = $this->config->getString('ldap.basedn', 'ou=groups,dc=example,dc=com');
206
        Logger::debug(
207
            $this->title . "Searching for groups in ldap.basedn " . $openldap_base . " with filter (" .
208
            $map['memberof'] . "=" . $attributes[$map['username']][0] . ") and attributes " . $map['member']
209
        );
210
211
        $username   = $this->config->getString('search.username');
212
        $password   = $this->config->getString('search.password', null);
213
214
        $ldapUtils = new LdapUtils();
215
        $ldapUtils->bind($this->ldap, $username, $password);
0 ignored issues
show
Bug Best Practice introduced by
The property ldap does not exist on SimpleSAML\Module\ldap\A...AttributeAddUsersGroups. Did you maybe forget to declare it?
Loading history...
216
217
        $groups = [];
218
        try {
219
            /* Intention is to filter in 'ou=groups,dc=example,dc=com' for
220
             * '(memberUid = <value of attribute.username>)' and take only the attributes 'cn' (=name of the group)
221
             */
222
            $all_groups = $ldapUtils->searchForMultiple(
0 ignored issues
show
Bug introduced by
The call to SimpleSAML\Module\ldap\U...ap::searchForMultiple() has too few arguments starting with options. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
            /** @scrutinizer ignore-call */ 
223
            $all_groups = $ldapUtils->searchForMultiple(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
223
                $openldap_base,
0 ignored issues
show
Bug introduced by
It seems like $openldap_base can also be of type string; however, parameter $ldap of SimpleSAML\Module\ldap\U...ap::searchForMultiple() does only seem to accept Symfony\Component\Ldap\Ldap, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

223
                /** @scrutinizer ignore-type */ $openldap_base,
Loading history...
224
                array_merge(
225
                    [
226
                        $map['memberof'] => $attributes[$map['username']][0]
227
                    ],
228
                    $this->additional_filters
229
                ),
230
                [$map['return']]
0 ignored issues
show
Bug introduced by
array($map['return']) of type array is incompatible with the type string expected by parameter $filter of SimpleSAML\Module\ldap\U...ap::searchForMultiple(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
                /** @scrutinizer ignore-type */ [$map['return']]
Loading history...
231
            );
232
        } catch (Error\UserNotFound $e) {
233
            return $groups; // if no groups found return with empty (still just initialized) groups array
234
        }
235
236
        // run through all groups and add each to our groups array
237
        foreach ($all_groups as $group_entry) {
238
            $groups[] = $group_entry[$map['return']][0];
239
        }
240
241
        return $groups;
242
    }
243
244
245
    /**
246
     * Active Directory optimized search
247
     * using the required attribute values from the user to
248
     * get their group membership, recursively.
249
     *
250
     * @throws \SimpleSAML\Error\Exception
251
     * @param array $attributes
252
     * @return array
253
     */
254
    protected function getGroupsActiveDirectory(array $attributes): array
255
    {
256
        // Log the AD specific search
257
        Logger::debug(
258
            $this->title . 'Searching LDAP using ActiveDirectory specific method.'
259
        );
260
261
        // Reference the map, just to make the name shorter
262
        $map = &$this->attribute_map;
263
264
        // Make sure the defined dn attribute exists
265
        if (!isset($attributes[$map['dn']])) {
266
            throw new Error\Exception(
267
                $this->title . 'The DN attribute [' . $map['dn'] .
268
                '] is not defined in the user\'s Attributes: ' . implode(', ', array_keys($attributes))
269
            );
270
        }
271
272
        // DN attribute must have a value
273
        if (!isset($attributes[$map['dn']][0]) || !$attributes[$map['dn']][0]) {
274
            throw new Error\Exception(
275
                $this->title . 'The DN attribute [' . $map['dn'] .
276
                '] does not have a [0] value defined. ' . $this->varExport($attributes[$map['dn']])
277
            );
278
        }
279
280
        // Pass to the AD specific search
281
        return $this->searchActiveDirectory($attributes[$map['dn']][0]);
282
    }
283
284
285
    /**
286
     * Looks for groups from the list of DN's passed. Also
287
     * recursively searches groups for further membership.
288
     * Avoids loops by only searching a DN once. Returns
289
     * the list of groups found.
290
     *
291
     * @param array $memberof
292
     * @return array
293
     */
294
    protected function search(array $memberof): array
295
    {
296
        // Used to determine what DN's have already been searched
297
        static $searched = [];
298
299
        // Init the groups variable
300
        $groups = [];
301
302
        // Shorten the variable name
303
        $map = &$this->attribute_map;
304
305
        // Log the search
306
        Logger::debug(
307
            $this->title . 'Checking DNs for groups.' .
308
            ' DNs: ' . implode('; ', $memberof) .
309
            ' Attributes: ' . $map['memberof'] . ', ' . $map['type'] .
310
            ' Group Type: ' . $this->type_map['group']
311
        );
312
313
        // Work out what attributes to get for a group
314
        $use_group_name = false;
315
        $get_attributes = [$map['memberof'], $map['type']];
316
        if (isset($map['name']) && $map['name']) {
317
            $get_attributes[] = $map['name'];
318
            $use_group_name = true;
319
        }
320
321
        // Check each DN of the passed memberOf
322
        foreach ($memberof as $dn) {
323
            // Avoid infinite loops, only need to check a DN once
324
            if (isset($searched[$dn])) {
325
                continue;
326
            }
327
328
            // Track all DN's that are searched
329
            // Use DN for key as well, isset() is faster than in_array()
330
            $searched[$dn] = $dn;
331
332
            // Query LDAP for the attribute values for the DN
333
            try {
334
                $attributes = $this->getLdap()->getAttributes($dn, $get_attributes);
0 ignored issues
show
Bug introduced by
The method getLdap() does not exist on SimpleSAML\Module\ldap\A...AttributeAddUsersGroups. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

334
                $attributes = $this->/** @scrutinizer ignore-call */ getLdap()->getAttributes($dn, $get_attributes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
335
            } catch (Error\AuthSource $e) {
336
                continue; // DN must not exist, just continue. Logged by the LDAP object
337
            }
338
339
            // Only look for groups
340
            if (!in_array($this->type_map['group'], $attributes[$map['type']], true)) {
341
                continue;
342
            }
343
344
            // Add to found groups array
345
            if ($use_group_name && isset($attributes[$map['name']]) && is_array($attributes[$map['name']])) {
346
                $groups[] = $attributes[$map['name']][0];
347
            } else {
348
                $groups[] = $dn;
349
            }
350
351
            // Recursively search "sub" groups
352
            if (!empty($attributes[$map['memberof']])) {
353
                $groups = array_merge($groups, $this->search($attributes[$map['memberof']]));
354
            }
355
        }
356
357
        // Return only the unique group names
358
        return array_unique($groups);
359
    }
360
361
362
    /**
363
     * Searches LDAP using a ActiveDirectory specific filter,
364
     * looking for group membership for the users DN. Returns
365
     * the list of group DNs retrieved.
366
     *
367
     * @param string $dn
368
     * @return array
369
     */
370
    protected function searchActiveDirectory(string $dn): array
371
    {
372
        $arrayUtils = new Utils\Arrays();
373
374
        // Shorten the variable name
375
        $map = &$this->attribute_map;
376
377
        // Log the search
378
        Logger::debug(
379
            $this->title . 'Searching ActiveDirectory group membership.' .
380
            ' DN: ' . $dn .
381
            ' DN Attribute: ' . $map['dn'] .
382
            ' Return Attribute: ' . $map['return'] .
383
            ' Member Attribute: ' . $map['member'] .
384
            ' Type Attribute: ' . $map['type'] .
385
            ' Type Value: ' . $this->type_map['group'] .
386
            ' Base: ' . implode('; ', $arrayUtils->arrayize($this->base_dn))
0 ignored issues
show
Bug Best Practice introduced by
The property base_dn does not exist on SimpleSAML\Module\ldap\A...AttributeAddUsersGroups. Did you maybe forget to declare it?
Loading history...
387
        );
388
389
        // AD connections should have this set
390
        $this->getLdap()->setOption(LDAP_OPT_REFERRALS, 0);
391
392
        // Search AD with the specific recursive flag
393
        try {
394
            $entries = $this->getLdap()->searchformultiple(
395
                $this->base_dn,
396
                array_merge(
397
                    [
398
                        $map['type'] => $this->type_map['group'],
399
                        $map['member'] . ':1.2.840.113556.1.4.1941:' => $dn
400
                    ],
401
                    $this->additional_filters
402
                ),
403
                [$map['return']]
404
            );
405
406
        // The search may throw an exception if no entries
407
        // are found, unlikely but possible.
408
        } catch (Error\UserNotFound $e) {
409
            return [];
410
        }
411
412
        //Init the groups
413
        $groups = [];
414
415
        // Check each entry..
416
        foreach ($entries as $entry) {
417
            // Check for the DN using the original attribute name
418
            if (isset($entry[$map['return']][0])) {
419
                $groups[] = $entry[$map['return']][0];
420
                continue;
421
            }
422
423
            // Sometimes the returned attribute names are lowercase
424
            if (isset($entry[strtolower($map['return'])][0])) {
425
                $groups[] = $entry[strtolower($map['return'])][0];
426
                continue;
427
            }
428
429
            // AD queries also seem to return the objects dn by default
430
            if (isset($entry['return'])) {
431
                $groups[] = $entry['return'];
432
                continue;
433
            }
434
435
            // Could not find DN, log and continue
436
            Logger::notice(
437
                $this->title . 'The return attribute [' .
438
                implode(', ', [$map['return'], strtolower($map['return'])]) .
439
                '] could not be found in the entry. ' . $this->varExport($entry)
440
            );
441
        }
442
443
        // All done
444
        return $groups;
445
    }
446
}
447