Completed
Push — master ( 77744b...07719b )
by Sean
02:20
created

LDAPGateway::resetPassword()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 2
eloc 7
nc 2
nop 2
1
<?php
2
/**
3
 * Class LDAPGateway
4
 *
5
 * Works within the LDAP domain model to provide basic operations.
6
 * These are exclusively used in LDAPService for constructing more complex operations.
7
 */
8
class LDAPGateway extends Object
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
9
{
10
    /**
11
     * @var array
12
     * @config
13
     */
14
    private static $options = array();
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...
15
16
    /**
17
     * @var Zend\Ldap\Ldap
18
     */
19
    private $ldap;
20
21
    public function __construct()
22
    {
23
        parent::__construct();
24
        // due to dependency injection this class can be created without any LDAP options set
25
        // and \Zend\Ldap\Ldap will throw a warning with an empty array
26
        if (count($this->config()->options)) {
27
            $this->ldap = new Zend\Ldap\Ldap($this->config()->options);
28
        }
29
    }
30
31
    /**
32
     * Query the LDAP directory with the given filter.
33
     *
34
     * @param string $filter The string to filter by, e.g. (objectClass=user)
35
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
36
     * @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
37
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
38
     * @param string $sort Sort results by this attribute if given
39
     * @return array
40
     */
41
    protected function search($filter, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array(), $sort = '')
42
    {
43
        $records = $this->ldap->search($filter, $baseDn, $scope, $attributes, $sort);
44
45
        $results = array();
46
        foreach ($records as $record) {
47
            foreach ($record as $attribute => $value) {
0 ignored issues
show
Bug introduced by
The expression $record of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
48
                // if the value is an array with a single value, e.g. 'samaccountname' => array(0 => 'myusername')
49
                // then make sure it's just set in the results as 'samaccountname' => 'myusername' so that it
50
                // can be used directly by ArrayData
51
                if (is_array($value) && count($value) == 1) {
52
                    $value = $value[0];
53
                }
54
55
                // ObjectGUID and ObjectSID attributes are in binary, we need to convert those to strings
56
                if ($attribute == 'objectguid') {
57
                    $value = LDAPUtil::bin_to_str_guid($value);
58
                }
59
                if ($attribute == 'objectsid') {
60
                    $value = LDAPUtil::bin_to_str_sid($value);
61
                }
62
63
                $record[$attribute] = $value;
64
            }
65
66
            $results[] = $record;
67
        }
68
69
        return $results;
70
    }
71
72
    /**
73
     * Authenticate the given username and password with LDAP.
74
     *
75
     * @param string $username
76
     * @param string $password
77
     * @return \Zend\Authentication\Result
78
     */
79
    public function authenticate($username, $password)
80
    {
81
        $auth = new Zend\Authentication\AuthenticationService();
82
        $adapter = new Zend\Authentication\Adapter\Ldap(array($this->config()->options), $username, $password);
83
        return $auth->authenticate($adapter);
84
    }
85
86
    /**
87
     * Query for LDAP nodes (organizational units, containers, and domains).
88
     *
89
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
90
     * @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
91
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
92
     * @param string $sort Sort results by this attribute if given
93
     * @return array
94
     */
95
    public function getNodes($baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array(), $sort = '')
96
    {
97
        return $this->search('(|(objectClass=organizationalUnit)(objectClass=container)(objectClass=domain))', $baseDn, $scope, $attributes, $sort);
98
    }
99
100
    /**
101
     * Query for LDAP groups.
102
     *
103
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
104
     * @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
105
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
106
     * @param string $sort Sort results by this attribute if given
107
     * @return array
108
     */
109
    public function getGroups($baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array(), $sort = '')
110
    {
111
        return $this->search('(objectClass=group)', $baseDn, $scope, $attributes, $sort);
112
    }
113
114
    /**
115
     * Return all nested AD groups underneath a specific DN
116
     *
117
     * @param string $dn
118
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
119
     * @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
120
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
121
     * @return array
122
     */
123
    public function getNestedGroups($dn, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
124
    {
125
        return $this->search(
126
            sprintf('(&(objectClass=group)(memberOf:1.2.840.113556.1.4.1941:=%s))', $dn),
127
            $baseDn,
128
            $scope,
129
            $attributes
130
        );
131
    }
132
133
    /**
134
     * Return a particular LDAP group by objectGUID value.
135
     *
136
     * @param string $guid
137
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
138
     * @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
139
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
140
     * @return array
141
     */
142 View Code Duplication
    public function getGroupByGUID($guid, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
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...
143
    {
144
        return $this->search(
145
            sprintf('(&(objectClass=group)(objectGUID=%s))', LDAPUtil::str_to_hex_guid($guid, true)),
146
            $baseDn,
147
            $scope,
148
            $attributes
149
        );
150
    }
151
152
    /**
153
     * Return a particular LDAP group by DN value.
154
     *
155
     * @param string $dn
156
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
157
     * @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
158
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
159
     * @return array
160
     */
161
    public function getGroupByDN($dn, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
162
    {
163
        return $this->search(
164
            sprintf('(&(objectClass=group)(distinguishedname=%s))', $dn),
165
            $baseDn,
166
            $scope,
167
            $attributes
168
        );
169
    }
170
171
    /**
172
     * Query for LDAP users, but don't include built-in user accounts.
173
     *
174
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
175
     * @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
176
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
177
     * @param string $sort Sort results by this attribute if given
178
     * @return array
179
     */
180
    public function getUsers($baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array(), $sort = '')
181
    {
182
        return $this->search(
183
            '(&(objectClass=user)(!(objectClass=computer))(!(samaccountname=Guest))(!(samaccountname=Administrator))(!(samaccountname=krbtgt)))',
184
            $baseDn,
185
            $scope,
186
            $attributes,
187
            $sort
188
        );
189
    }
190
191
    /**
192
     * Return a particular LDAP user by objectGUID value.
193
     *
194
     * @param string $guid
195
     * @return array
196
     */
197 View Code Duplication
    public function getUserByGUID($guid, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
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...
198
    {
199
        return $this->search(
200
            sprintf('(&(objectClass=user)(objectGUID=%s))', LDAPUtil::str_to_hex_guid($guid, true)),
201
            $baseDn,
202
            $scope,
203
            $attributes
204
        );
205
    }
206
207
    /**
208
     * Return a particular LDAP user by DN value.
209
     *
210
     * @param string $dn
211
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
212
     * @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
213
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
214
     * @return array
215
     */
216
    public function getUserByDN($dn, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
217
    {
218
        return $this->search(
219
            sprintf('(&(objectClass=user)(distinguishedname=%s))', $dn),
220
            $baseDn,
221
            $scope,
222
            $attributes
223
        );
224
    }
225
226
    /**
227
     * Return a particular LDAP user by mail value.
228
     *
229
     * @param string $email
230
     * @return array
231
     */
232
    public function getUserByEmail($email, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
233
    {
234
        return $this->search(
235
            sprintf('(&(objectClass=user)(mail=%s))', Zend\Ldap\Filter\AbstractFilter::escapeValue($email)),
236
            $baseDn,
237
            $scope,
238
            $attributes
239
        );
240
    }
241
242
    /**
243
     * Get a specific user's data from LDAP
244
     *
245
     * @param string $username
246
     * @param null|string $baseDn The DN to search from. Default is the baseDn option in the connection if not given
247
     * @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
248
     * @param array $attributes Restrict to specific AD attributes. An empty array will return all attributes
249
     * @return array
250
     * @throws Exception
251
     */
252
    public function getUserByUsername($username, $baseDn = null, $scope = Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes = array())
253
    {
254
        $options = $this->config()->options;
255
        $option = isset($options['accountCanonicalForm']) ? $options['accountCanonicalForm'] : null;
256
257
        // will translate the username to [email protected], username or foo\user depending on the
258
        // $options['accountCanonicalForm']
259
        $username = $this->ldap->getCanonicalAccountName($username, $option);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $username. This often makes code more readable.
Loading history...
260
        switch ($option) {
261
            case Zend\Ldap\Ldap::ACCTNAME_FORM_USERNAME: // traditional style usernames, e.g. alice
262
                $filter = sprintf('(&(objectClass=user)(samaccountname=%s))', Zend\Ldap\Filter\AbstractFilter::escapeValue($username));
263
                break;
264
            case Zend\Ldap\Ldap::ACCTNAME_FORM_BACKSLASH: // backslash style usernames, e.g. FOO\alice
265
                // @todo Not supported yet!
266
                throw new Exception('Backslash style not supported in LDAPGateway::getUserByUsername()!');
267
                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...
268
            case Zend\Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL: // principal style usernames, e.g. [email protected]
269
                $filter = sprintf('(&(objectClass=user)(userprincipalname=%s))', Zend\Ldap\Filter\AbstractFilter::escapeValue($username));
270
                break;
271
            case Zend\Ldap\Ldap::ACCTNAME_FORM_DN: // distinguished name, e.g. CN=someone,DC=example,DC=co,DC=nz
272
                // @todo Not supported yet!
273
                throw new Exception('DN style not supported in LDAPGateway::getUserByUsername()!');
274
                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...
275
            default: // default to principal style
276
                $filter = sprintf('(&(objectClass=user)(userprincipalname=%s))', Zend\Ldap\Filter\AbstractFilter::escapeValue($username));
277
        }
278
279
        return $this->search($filter, $baseDn, $scope, $attributes);
280
    }
281
282
    /**
283
     * Get a canonical username from the record based on the connection settings.
284
     *
285
     * @param array $data
286
     * @return string
287
     */
288
    public function getCanonicalUsername($data)
289
    {
290
        $options = $this->config()->options;
291
        $option = isset($options['accountCanonicalForm']) ? $options['accountCanonicalForm'] : null;
292
293
        switch ($option) {
294
            case Zend\Ldap\Ldap::ACCTNAME_FORM_USERNAME: // traditional style usernames, e.g. alice
295
                if (empty($data['samaccountname'])) {
296
                    throw new \Exception('Could not extract canonical username: samaccountname field missing');
297
                }
298
                return $data['samaccountname'];
299
            case Zend\Ldap\Ldap::ACCTNAME_FORM_BACKSLASH: // backslash style usernames, e.g. FOO\alice
300
                // @todo Not supported yet!
301
                throw new Exception('Backslash style not supported in LDAPGateway::getUsernameByEmail()!');
302
            case Zend\Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL: // principal style usernames, e.g. [email protected]
303
                if (empty($data['userprincipalname'])) {
304
                    throw new \Exception('Could not extract canonical username: userprincipalname field missing');
305
                }
306
                return $data['userprincipalname'];
307
            default: // default to principal style
308
                if (empty($data['userprincipalname'])) {
309
                    throw new \Exception('Could not extract canonical username: userprincipalname field missing');
310
                }
311
                return $data['userprincipalname'];
312
        }
313
    }
314
315
    /**
316
     * Changes user password via LDAP.
317
     *
318
     * Change password is different to administrative password reset in that it will respect the password
319
     * history policy. This is achieved by sending a remove followed by an add in one batch (which is different
320
     * to an ordinary attribute modification operation).
321
     *
322
     * @param string $dn Location to update the entry at.
323
     * @param string $password New password to set.
324
     * @param string $oldPassword Old password is needed to trigger a password change.
325
     * @throws \Zend\Ldap\Exception\LdapException
326
     */
327
    public function changePassword($dn, $password, $oldPassword)
328
    {
329
        if (!function_exists('ldap_modify_batch')) {
330
            // Password change is unsupported - missing PHP API method.
331
            $this->resetPassword($dn, $password);
332
            return;
333
        }
334
335
        $modifications = [
336
            [
337
                "attrib"  => "unicodePwd",
338
                "modtype" => LDAP_MODIFY_BATCH_REMOVE,
339
                "values"  => [iconv('UTF-8', 'UTF-16LE', sprintf('"%s"', $oldPassword))],
340
            ],
341
            [
342
                "attrib"  => "unicodePwd",
343
                "modtype" => LDAP_MODIFY_BATCH_ADD,
344
                "values"  => [iconv('UTF-8', 'UTF-16LE', sprintf('"%s"', $password))],
345
            ],
346
        ];
347
        // Batch attribute operations are not supported by Zend_Ldap, use raw resource.
348
        $ldapConn = $this->ldap->getResource();
349
        \Zend\Stdlib\ErrorHandler::start(E_WARNING);
350
        $succeeded = ldap_modify_batch($ldapConn, $dn, $modifications);
351
        \Zend\Stdlib\ErrorHandler::stop();
352
        if (!$succeeded) {
353
            throw new Exception($this->getLastPasswordError());
354
        }
355
    }
356
357
    /**
358
     * Administrative password reset.
359
     *
360
     * This is different to password change - it does not require old password, but also
361
     * it does not respect password history policy setting.
362
     *
363
     * @param string $dn Location to update the entry at.
364
     * @param string $password New password to set.
365
     * @throws \Zend\Ldap\Exception\LdapException
366
     */
367
    public function resetPassword($dn, $password)
368
    {
369
        try {
370
            $this->update(
371
                $dn,
372
                array('unicodePwd' => iconv('UTF-8', 'UTF-16LE', sprintf('"%s"', $password)))
373
            );
374
        } catch(\Zend\Ldap\Exception\LdapException $e) {
375
            throw new Exception($this->getLastPasswordError());
376
        }
377
    }
378
379
    /**
380
     * Updates an LDAP object.
381
     *
382
     * For this work you might need that LDAP connection is bind:ed with a user with
383
     * enough permissions to change attributes and that the LDAP connection is using
384
     * SSL/TLS. It depends on the server setup.
385
     *
386
     * If there are some errors, the underlying LDAP library should throw an Exception
387
     *
388
     * @param string $dn Location to update the entry at.
389
     * @param array $attributes An associative array of attributes.
390
     * @throws \Zend\Ldap\Exception\LdapException
391
     */
392
    public function update($dn, array $attributes)
393
    {
394
        $this->ldap->update($dn, $attributes);
395
    }
396
397
    /**
398
     * Deletes an LDAP object.
399
     *
400
     * @param string $dn Location of object to delete
401
     * @param bool $recursively Recursively delete nested objects?
402
     * @throws \Zend\Ldap\Exception\LdapException
403
     */
404
    public function delete($dn, $recursively = false)
405
    {
406
        $this->ldap->delete($dn, $recursively);
407
    }
408
409
    /**
410
     * Move an LDAP object from it's original DN location to another.
411
     * This effectively copies the object then deletes the original one.
412
     *
413
     * @param string $fromDn
414
     * @param string $toDn
415
     * @param bool $recursively Recursively move nested objects?
416
     */
417
    public function move($fromDn, $toDn, $recursively = false)
418
    {
419
        $this->ldap->move($fromDn, $toDn, $recursively);
420
    }
421
422
    /**
423
     * Add an LDAP object.
424
     *
425
     * For this work you might need that LDAP connection is bind:ed with a user with
426
     * enough permissions to change attributes and that the LDAP connection is using
427
     * SSL/TLS. It depends on the server setup.
428
     *
429
     * @param string $dn Location to add the entry at.
430
     * @param array $attributes An associative array of attributes.
431
     * @throws \Zend\Ldap\Exception\LdapException
432
     */
433
    public function add($dn, array $attributes)
434
    {
435
        $this->ldap->add($dn, $attributes);
436
    }
437
438
    /**
439
     * @param \Zend\Ldap\Exception\LdapException
440
     * @return string
441
     */
442
    private function getLastPasswordError() {
443
        $defaultError = _t('LDAPAuthenticator.CANTCHANGEPASSWORD', 'We couldn\'t change your password, please contact an administrator.');
444
        $error = '';
445
        ldap_get_option($this->ldap->getResource(), LDAP_OPT_ERROR_STRING, $error);
446
447
        if (!$error) {
448
            return $defaultError;
449
        }
450
451
        // Try to parse the exception to get the error message to display to user, eg:
452
        // 0000052D: Constraint violation - check_password_restrictions: the password does not meet the complexity criteria!)
453
        // 0000052D: Constraint violation - check_password_restrictions: the password was already used (in history)!)
454
455
        // We are only interested in the explanatory message after the last colon.
456
        $message = preg_replace('/.*:/', '', $error);
457
        if ($error) {
458
            return ucfirst(trim($message));
459
        }
460
461
        return $defaultError;
462
    }
463
}
464