Completed
Push — master ( 197387...875417 )
by Damian
11s
created

LDAPGateway   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 464
Duplicated Lines 3.88 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 7
dl 18
loc 464
rs 8.3673
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
C search() 0 30 7
A authenticate() 0 6 1
A getNodes() 0 4 1
A getGroups() 0 4 1
A getNestedGroups() 0 9 1
A getGroupByGUID() 9 9 1
A getGroupByDN() 0 9 1
A getUsers() 0 10 1
A getUserByGUID() 9 9 1
A getUserByDN() 0 9 1
A getUserByEmail() 0 9 1
B getUserByUsername() 0 30 6
C getCanonicalUsername() 0 26 8
B changePassword() 0 29 3
A resetPassword() 0 11 2
A update() 0 4 1
A delete() 0 4 1
A move() 0 4 1
A add() 0 4 1
B getLastPasswordError() 0 25 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LDAPGateway often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LDAPGateway, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\ActiveDirectory\Model;
4
5
use Exception;
6
use SilverStripe\Core\Object;
7
use Zend\Authentication\Adapter\Ldap as LdapAdapter;
8
use Zend\Authentication\AuthenticationService;
9
use Zend\Ldap\Exception\LdapException;
10
use Zend\Ldap\Filter\AbstractFilter;
11
use Zend\Ldap\Ldap;
12
use Zend\Stdlib\ErrorHandler;
13
14
/**
15
 * Class LDAPGateway
16
 *
17
 * Works within the LDAP domain model to provide basic operations.
18
 * These are exclusively used in LDAPService for constructing more complex operations.
19
 */
20
class LDAPGateway extends Object
21
{
22
    /**
23
     * @var array
24
     * @config
25
     */
26
    private static $options = [];
0 ignored issues
show
Unused Code introduced by
The property $options is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
27
28
    /**
29
     * @var Zend\Ldap\Ldap
30
     */
31
    private $ldap;
32
33
    public function __construct()
34
    {
35
        parent::__construct();
36
        // due to dependency injection this class can be created without any LDAP options set
37
        // and \Zend\Ldap\Ldap will throw a warning with an empty array
38
        if (count($this->config()->options)) {
39
            $this->ldap = new Ldap($this->config()->options);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Zend\Ldap\Ldap($this->config()->options) of type object<Zend\Ldap\Ldap> is incompatible with the declared type object<SilverStripe\Acti...y\Model\Zend\Ldap\Ldap> of property $ldap.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
40
        }
41
    }
42
43
    /**
44
     * Query the LDAP directory with the given filter.
45
     *
46
     * @param string $filter The string to filter by, e.g. (objectClass=user)
47
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
48
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE.
49
     *                   Default is Zend_Ldap::SEARCH_SCOPE_SUB
50
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
51
     * @param string $sort Sort results by this attribute if given
52
     * @return array
53
     */
54
    protected function search($filter, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [], $sort = '')
55
    {
56
        $records = $this->ldap->search($filter, $baseDn, $scope, $attributes, $sort);
57
58
        $results = [];
59
        foreach ($records as $record) {
60
            foreach ($record as $attribute => $value) {
61
                // if the value is an array with a single value, e.g. 'samaccountname' => array(0 => 'myusername')
62
                // then make sure it's just set in the results as 'samaccountname' => 'myusername' so that it
63
                // can be used directly by ArrayData
64
                if (is_array($value) && count($value) == 1) {
65
                    $value = $value[0];
66
                }
67
68
                // ObjectGUID and ObjectSID attributes are in binary, we need to convert those to strings
69
                if ($attribute == 'objectguid') {
70
                    $value = LDAPUtil::bin_to_str_guid($value);
71
                }
72
                if ($attribute == 'objectsid') {
73
                    $value = LDAPUtil::bin_to_str_sid($value);
74
                }
75
76
                $record[$attribute] = $value;
77
            }
78
79
            $results[] = $record;
80
        }
81
82
        return $results;
83
    }
84
85
    /**
86
     * Authenticate the given username and password with LDAP.
87
     *
88
     * @param string $username
89
     * @param string $password
90
     * @return \Zend\Authentication\Result
91
     */
92
    public function authenticate($username, $password)
93
    {
94
        $auth = new AuthenticationService();
95
        $adapter = new LdapAdapter([$this->config()->options], $username, $password);
96
        return $auth->authenticate($adapter);
97
    }
98
99
    /**
100
     * Query for LDAP nodes (organizational units, containers, and domains).
101
     *
102
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
103
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
104
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
105
     * @param string $sort Sort results by this attribute if given
106
     * @return array
107
     */
108
    public function getNodes($baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [], $sort = '')
109
    {
110
        return $this->search('(|(objectClass=organizationalUnit)(objectClass=container)(objectClass=domain))', $baseDn, $scope, $attributes, $sort);
111
    }
112
113
    /**
114
     * Query for LDAP groups.
115
     *
116
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
117
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
118
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
119
     * @param string $sort Sort results by this attribute if given
120
     * @return array
121
     */
122
    public function getGroups($baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [], $sort = '')
123
    {
124
        return $this->search('(objectClass=group)', $baseDn, $scope, $attributes, $sort);
125
    }
126
127
    /**
128
     * Return all nested AD groups underneath a specific DN
129
     *
130
     * @param string $dn
131
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
132
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
133
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
134
     * @return array
135
     */
136
    public function getNestedGroups($dn, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
137
    {
138
        return $this->search(
139
            sprintf('(&(objectClass=group)(memberOf:1.2.840.113556.1.4.1941:=%s))', $dn),
140
            $baseDn,
141
            $scope,
142
            $attributes
143
        );
144
    }
145
146
    /**
147
     * Return a particular LDAP group by objectGUID value.
148
     *
149
     * @param string $guid
150
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
151
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
152
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
153
     * @return array
154
     */
155 View Code Duplication
    public function getGroupByGUID($guid, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
    {
157
        return $this->search(
158
            sprintf('(&(objectClass=group)(objectGUID=%s))', LDAPUtil::str_to_hex_guid($guid, true)),
159
            $baseDn,
160
            $scope,
161
            $attributes
162
        );
163
    }
164
165
    /**
166
     * Return a particular LDAP group by DN value.
167
     *
168
     * @param string $dn
169
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
170
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
171
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
172
     * @return array
173
     */
174
    public function getGroupByDN($dn, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
175
    {
176
        return $this->search(
177
            sprintf('(&(objectClass=group)(distinguishedname=%s))', $dn),
178
            $baseDn,
179
            $scope,
180
            $attributes
181
        );
182
    }
183
184
    /**
185
     * Query for LDAP users, but don't include built-in user accounts.
186
     *
187
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
188
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
189
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
190
     * @param string $sort Sort results by this attribute if given
191
     * @return array
192
     */
193
    public function getUsers($baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = [], $sort = '')
194
    {
195
        return $this->search(
196
            '(&(objectClass=user)(!(objectClass=computer))(!(samaccountname=Guest))(!(samaccountname=Administrator))(!(samaccountname=krbtgt)))',
197
            $baseDn,
198
            $scope,
199
            $attributes,
200
            $sort
201
        );
202
    }
203
204
    /**
205
     * Return a particular LDAP user by objectGUID value.
206
     *
207
     * @param string $guid
208
     * @return array
209
     */
210 View Code Duplication
    public function getUserByGUID($guid, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
    {
212
        return $this->search(
213
            sprintf('(&(objectClass=user)(objectGUID=%s))', LDAPUtil::str_to_hex_guid($guid, true)),
214
            $baseDn,
215
            $scope,
216
            $attributes
217
        );
218
    }
219
220
    /**
221
     * Return a particular LDAP user by DN value.
222
     *
223
     * @param string $dn
224
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
225
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
226
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
227
     * @return array
228
     */
229
    public function getUserByDN($dn, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
230
    {
231
        return $this->search(
232
            sprintf('(&(objectClass=user)(distinguishedname=%s))', $dn),
233
            $baseDn,
234
            $scope,
235
            $attributes
236
        );
237
    }
238
239
    /**
240
     * Return a particular LDAP user by mail value.
241
     *
242
     * @param string $email
243
     * @return array
244
     */
245
    public function getUserByEmail($email, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
246
    {
247
        return $this->search(
248
            sprintf('(&(objectClass=user)(mail=%s))', AbstractFilter::escapeValue($email)),
249
            $baseDn,
250
            $scope,
251
            $attributes
252
        );
253
    }
254
255
    /**
256
     * Get a specific user's data from LDAP
257
     *
258
     * @param string $username
259
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
260
     * @param int $scope The scope to perform the search. Zend_Ldap::SEARCH_SCOPE_ONE, Zend_LDAP::SEARCH_SCOPE_BASE. Default is Zend_Ldap::SEARCH_SCOPE_SUB
261
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
262
     * @return array
263
     * @throws Exception
264
     */
265
    public function getUserByUsername($username, $baseDn = null, $scope = Ldap::SEARCH_SCOPE_SUB, $attributes = [])
266
    {
267
        $options = $this->config()->options;
268
        $option = isset($options['accountCanonicalForm']) ? $options['accountCanonicalForm'] : null;
269
270
        // will translate the username to [email protected], username or foo\user depending on the
271
        // $options['accountCanonicalForm']
272
        $username = $this->ldap->getCanonicalAccountName($username, $option);
273
        switch ($option) {
274
            case Ldap::ACCTNAME_FORM_USERNAME: // traditional style usernames, e.g. alice
275
                $filter = sprintf('(&(objectClass=user)(samaccountname=%s))', AbstractFilter::escapeValue($username));
276
                break;
277
            case Ldap::ACCTNAME_FORM_BACKSLASH: // backslash style usernames, e.g. FOO\alice
278
                // @todo Not supported yet!
279
                throw new Exception('Backslash style not supported in LDAPGateway::getUserByUsername()!');
280
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
281
            case Ldap::ACCTNAME_FORM_PRINCIPAL: // principal style usernames, e.g. [email protected]
282
                $filter = sprintf('(&(objectClass=user)(userprincipalname=%s))', AbstractFilter::escapeValue($username));
283
                break;
284
            case Ldap::ACCTNAME_FORM_DN: // distinguished name, e.g. CN=someone,DC=example,DC=co,DC=nz
285
                // @todo Not supported yet!
286
                throw new Exception('DN style not supported in LDAPGateway::getUserByUsername()!');
287
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
288
            default: // default to principal style
289
                $filter = sprintf('(&(objectClass=user)(userprincipalname=%s))', AbstractFilter::escapeValue($username));
290
                break;
291
        }
292
293
        return $this->search($filter, $baseDn, $scope, $attributes);
294
    }
295
296
    /**
297
     * Get a canonical username from the record based on the connection settings.
298
     *
299
     * @param  array $data
300
     * @return string
301
     * @throws Exception
302
     */
303
    public function getCanonicalUsername($data)
304
    {
305
        $options = $this->config()->options;
306
        $option = isset($options['accountCanonicalForm']) ? $options['accountCanonicalForm'] : null;
307
308
        switch ($option) {
309
            case Ldap::ACCTNAME_FORM_USERNAME: // traditional style usernames, e.g. alice
310
                if (empty($data['samaccountname'])) {
311
                    throw new \Exception('Could not extract canonical username: samaccountname field missing');
312
                }
313
                return $data['samaccountname'];
314
            case Ldap::ACCTNAME_FORM_BACKSLASH: // backslash style usernames, e.g. FOO\alice
315
                // @todo Not supported yet!
316
                throw new Exception('Backslash style not supported in LDAPGateway::getUsernameByEmail()!');
317
            case Ldap::ACCTNAME_FORM_PRINCIPAL: // principal style usernames, e.g. [email protected]
318
                if (empty($data['userprincipalname'])) {
319
                    throw new Exception('Could not extract canonical username: userprincipalname field missing');
320
                }
321
                return $data['userprincipalname'];
322
            default: // default to principal style
323
                if (empty($data['userprincipalname'])) {
324
                    throw new Exception('Could not extract canonical username: userprincipalname field missing');
325
                }
326
                return $data['userprincipalname'];
327
        }
328
    }
329
330
    /**
331
     * Changes user password via LDAP.
332
     *
333
     * Change password is different to administrative password reset in that it will respect the password
334
     * history policy. This is achieved by sending a remove followed by an add in one batch (which is different
335
     * to an ordinary attribute modification operation).
336
     *
337
     * @param string $dn Location to update the entry at.
338
     * @param string $password New password to set.
339
     * @param string $oldPassword Old password is needed to trigger a password change.
340
     * @throws \Zend\Ldap\Exception\LdapException
341
     * @throws Exception
342
     */
343
    public function changePassword($dn, $password, $oldPassword)
344
    {
345
        if (!function_exists('ldap_modify_batch')) {
346
            // Password change is unsupported - missing PHP API method.
347
            $this->resetPassword($dn, $password);
348
            return;
349
        }
350
351
        $modifications = [
352
            [
353
                'attrib'  => 'unicodePwd',
354
                'modtype' => LDAP_MODIFY_BATCH_REMOVE,
355
                'values'  => [iconv('UTF-8', 'UTF-16LE', sprintf('"%s"', $oldPassword))],
356
            ],
357
            [
358
                'attrib'  => 'unicodePwd',
359
                'modtype' => LDAP_MODIFY_BATCH_ADD,
360
                'values'  => [iconv('UTF-8', 'UTF-16LE', sprintf('"%s"', $password))],
361
            ],
362
        ];
363
        // Batch attribute operations are not supported by Zend_Ldap, use raw resource.
364
        $ldapConn = $this->ldap->getResource();
365
        ErrorHandler::start(E_WARNING);
366
        $succeeded = ldap_modify_batch($ldapConn, $dn, $modifications);
367
        ErrorHandler::stop();
368
        if (!$succeeded) {
369
            throw new Exception($this->getLastPasswordError());
370
        }
371
    }
372
373
    /**
374
     * Administrative password reset.
375
     *
376
     * This is different to password change - it does not require old password, but also
377
     * it does not respect password history policy setting.
378
     *
379
     * @param string $dn Location to update the entry at.
380
     * @param string $password New password to set.
381
     * @throws \Zend\Ldap\Exception\LdapException
382
     * @throws Exception
383
     */
384
    public function resetPassword($dn, $password)
385
    {
386
        try {
387
            $this->update(
388
                $dn,
389
                ['unicodePwd' => iconv('UTF-8', 'UTF-16LE', sprintf('"%s"', $password))]
390
            );
391
        } catch(LdapException $e) {
392
            throw new Exception($this->getLastPasswordError());
393
        }
394
    }
395
396
    /**
397
     * Updates an LDAP object.
398
     *
399
     * For this work you might need that LDAP connection is bind:ed with a user with
400
     * enough permissions to change attributes and that the LDAP connection is using
401
     * SSL/TLS. It depends on the server setup.
402
     *
403
     * If there are some errors, the underlying LDAP library should throw an Exception
404
     *
405
     * @param string $dn Location to update the entry at.
406
     * @param array $attributes An associative array of attributes.
407
     * @throws \Zend\Ldap\Exception\LdapException
408
     */
409
    public function update($dn, array $attributes)
410
    {
411
        $this->ldap->update($dn, $attributes);
412
    }
413
414
    /**
415
     * Deletes an LDAP object.
416
     *
417
     * @param string $dn Location of object to delete
418
     * @param bool $recursively Recursively delete nested objects?
419
     * @throws \Zend\Ldap\Exception\LdapException
420
     */
421
    public function delete($dn, $recursively = false)
422
    {
423
        $this->ldap->delete($dn, $recursively);
424
    }
425
426
    /**
427
     * Move an LDAP object from it's original DN location to another.
428
     * This effectively copies the object then deletes the original one.
429
     *
430
     * @param string $fromDn
431
     * @param string $toDn
432
     * @param bool $recursively Recursively move nested objects?
433
     */
434
    public function move($fromDn, $toDn, $recursively = false)
435
    {
436
        $this->ldap->move($fromDn, $toDn, $recursively);
437
    }
438
439
    /**
440
     * Add an LDAP object.
441
     *
442
     * For this work you might need that LDAP connection is bind:ed with a user with
443
     * enough permissions to change attributes and that the LDAP connection is using
444
     * SSL/TLS. It depends on the server setup.
445
     *
446
     * @param string $dn Location to add the entry at.
447
     * @param array $attributes An associative array of attributes.
448
     * @throws \Zend\Ldap\Exception\LdapException
449
     */
450
    public function add($dn, array $attributes)
451
    {
452
        $this->ldap->add($dn, $attributes);
453
    }
454
455
    /**
456
     * @return string
457
     */
458
    private function getLastPasswordError()
459
    {
460
        $defaultError = _t(
461
            'LDAPAuthenticator.CANTCHANGEPASSWORD',
462
            'We couldn\'t change your password, please contact an administrator.'
463
        );
464
        $error = '';
465
        ldap_get_option($this->ldap->getResource(), LDAP_OPT_ERROR_STRING, $error);
466
467
        if (!$error) {
468
            return $defaultError;
469
        }
470
471
        // Try to parse the exception to get the error message to display to user, eg:
472
        // 0000052D: Constraint violation - check_password_restrictions: the password does not meet the complexity criteria!)
473
        // 0000052D: Constraint violation - check_password_restrictions: the password was already used (in history)!)
474
475
        // We are only interested in the explanatory message after the last colon.
476
        $message = preg_replace('/.*:/', '', $error);
477
        if ($error) {
478
            return ucfirst(trim($message));
479
        }
480
481
        return $defaultError;
482
    }
483
}
484