LDAPService   F
last analyzed

Complexity

Total Complexity 160

Size/Duplication

Total Lines 1050
Duplicated Lines 11.14 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
wmc 160
lcom 1
cbo 19
dl 117
loc 1050
rs 1.4
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A get_cache() 0 7 1
A flush() 0 5 1
A setGateway() 0 4 1
A enabled() 0 5 1
B authenticate() 13 36 7
A getNodes() 0 17 4
B getGroups() 10 24 7
A getNestedGroups() 6 18 5
A getGroupByGUID() 10 10 4
A getGroupByDN() 10 10 4
A getUsers() 10 18 5
A getUserByGUID() 10 10 4
A getUserByDN() 10 10 4
A getUserByEmail() 10 10 4
A getUserByUsername() 10 10 4
A getUsernameByEmail() 0 9 2
A getLDAPGroupMembers() 0 13 4
F updateMemberFromLDAP() 18 166 33
B updateGroupFromLDAP() 0 36 6
B createLDAPUser() 0 42 6
B createLDAPGroup() 0 42 6
B updateLDAPFromMember() 0 59 10
F updateLDAPGroupsForMember() 0 74 18
A addLDAPUserToGroup() 0 16 3
B setPassword() 0 33 6
A deleteLDAPMember() 0 18 5
A update() 0 4 1
A delete() 0 4 1
A move() 0 4 1
A add() 0 4 1
A passwordHistoryWorkaround() 0 7 1

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 LDAPService 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 LDAPService, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Class LDAPService
4
 *
5
 * Provides LDAP operations expressed in terms of the SilverStripe domain.
6
 * All other modules should access LDAP through this class.
7
 *
8
 * This class builds on top of LDAPGateway's detailed code by adding:
9
 * - caching
10
 * - data aggregation and restructuring from multiple lower-level calls
11
 * - error handling
12
 *
13
 * LDAPService relies on Zend LDAP module's data structures for some parameters and some return values.
14
 */
15
class LDAPService extends Object implements Flushable
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...
16
{
17
    /**
18
     * @var array
19
     */
20
    private static $dependencies = [
0 ignored issues
show
Unused Code introduced by
The property $dependencies 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...
21
        'gateway' => '%$LDAPGateway'
22
    ];
23
24
    /**
25
     * If configured, only user objects within these locations will be exposed to this service.
26
     *
27
     * @var array
28
     * @config
29
     */
30
    private static $users_search_locations = [];
0 ignored issues
show
Unused Code introduced by
The property $users_search_locations 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...
31
32
    /**
33
     * If configured, only group objects within these locations will be exposed to this service.
34
     * @var array
35
     *
36
     * @config
37
     */
38
    private static $groups_search_locations = [];
0 ignored issues
show
Unused Code introduced by
The property $groups_search_locations 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...
39
40
    /**
41
     * Location to create new users in (distinguished name).
42
     * @var string
43
     *
44
     * @config
45
     */
46
    private static $new_users_dn;
0 ignored issues
show
Unused Code introduced by
The property $new_users_dn 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...
47
48
    /**
49
     * Location to create new groups in (distinguished name).
50
     * @var string
51
     *
52
     * @config
53
     */
54
    private static $new_groups_dn;
0 ignored issues
show
Unused Code introduced by
The property $new_groups_dn 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...
55
56
    /**
57
     * @var array
58
     */
59
    private static $_cache_nested_groups = [];
60
61
    /**
62
     * If this is configured to a "Code" value of a {@link Group} in SilverStripe, the user will always
63
     * be added to this group's membership when imported, regardless of any sort of group mappings.
64
     *
65
     * @var string
66
     * @config
67
     */
68
    private static $default_group;
0 ignored issues
show
Unused Code introduced by
The property $default_group 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...
69
70
    /**
71
     * For samba4 directory, there is no way to enforce password history on password resets.
72
     * This only happens with changePassword (which requires the old password).
73
     * This works around it by making the old password up and setting it administratively.
74
     *
75
     * A cleaner fix would be to use the LDAP_SERVER_POLICY_HINTS_OID connection flag,
76
     * but it's not implemented in samba https://bugzilla.samba.org/show_bug.cgi?id=12020
77
     *
78
     * @var bool
79
     */
80
    private static $password_history_workaround = false;
0 ignored issues
show
Unused Code introduced by
The property $password_history_workaround 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...
81
82
    /**
83
     * Get the cache objecgt used for LDAP results. Note that the default lifetime set here
84
     * is 8 hours, but you can change that by calling SS_Cache::set_lifetime('ldap', <lifetime in seconds>)
85
     *
86
     * @return Zend_Cache_Frontend
87
     */
88
    public static function get_cache()
89
    {
90
        return SS_Cache::factory('ldap', 'Output', [
91
            'automatic_serialization' => true,
92
            'lifetime' => 28800
93
        ]);
94
    }
95
96
    /**
97
     * Flushes out the LDAP results cache when flush=1 is called.
98
     */
99
    public static function flush()
100
    {
101
        $cache = self::get_cache();
102
        $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
103
    }
104
105
    /**
106
     * @var LDAPGateway
107
     */
108
    public $gateway;
109
110
    /**
111
     * Setter for gateway. Useful for overriding the gateway with a fake for testing.
112
     * @var LDAPGateway
113
     */
114
    public function setGateway($gateway)
115
    {
116
        $this->gateway = $gateway;
117
    }
118
119
    /**
120
     * Checkes whether or not the service is enabled.
121
     *
122
     * @return bool
123
     */
124
    public function enabled()
125
    {
126
        $options = Config::inst()->get('LDAPGateway', 'options');
127
        return !empty($options);
128
    }
129
130
    /**
131
     * Authenticate the given username and password with LDAP.
132
     *
133
     * @param string $username
134
     * @param string $password
135
     *
136
     * @return array
137
     */
138
    public function authenticate($username, $password)
139
    {
140
        $result = $this->gateway->authenticate($username, $password);
141
        $messages = $result->getMessages();
142
143
        // all messages beyond the first one are for debugging and
144
        // not suitable to display to the user.
145
        foreach ($messages as $i => $message) {
146
            if ($i > 0) {
147
                SS_Log::log(str_replace("\n", "\n  ", $message), SS_Log::DEBUG);
148
            }
149
        }
150
151
        $message = $messages[0]; // first message is user readable, suitable for showing on login form
152
153
        // show better errors than the defaults for various status codes returned by LDAP
154 View Code Duplication
        if (!empty($messages[1]) && strpos($messages[1], 'NT_STATUS_ACCOUNT_LOCKED_OUT') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
155
            $message = _t(
156
                'LDAPService.ACCOUNTLOCKEDOUT',
157
                'Your account has been temporarily locked because of too many failed login attempts. ' .
158
                'Please try again later.'
159
            );
160
        }
161 View Code Duplication
        if (!empty($messages[1]) && strpos($messages[1], 'NT_STATUS_LOGON_FAILURE') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
162
            $message = _t(
163
                'LDAPService.INVALIDCREDENTIALS',
164
                'The provided details don\'t seem to be correct. Please try again.'
165
            );
166
        }
167
168
        return [
169
            'success' => $result->getCode() === 1,
170
            'identity' => $result->getIdentity(),
171
            'message' => $message
172
        ];
173
    }
174
175
    /**
176
     * Return all nodes (organizational units, containers, and domains) within the current base DN.
177
     *
178
     * @param boolean $cached Cache the results from AD, so that subsequent calls are faster. Enabled by default.
179
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
180
     * @return array
181
     */
182
    public function getNodes($cached = true, $attributes = [])
183
    {
184
        $cache = self::get_cache();
185
        $results = $cache->load('nodes' . md5(implode('', $attributes)));
186
187
        if (!$results || !$cached) {
188
            $results = [];
189
            $records = $this->gateway->getNodes(null, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
190
            foreach ($records as $record) {
191
                $results[$record['dn']] = $record;
192
            }
193
194
            $cache->save($results);
195
        }
196
197
        return $results;
198
    }
199
200
    /**
201
     * Return all AD groups in configured search locations, including all nested groups.
202
     * Uses groups_search_locations if defined, otherwise falls back to NULL, which tells LDAPGateway
203
     * to use the default baseDn defined in the connection.
204
     *
205
     * @param boolean $cached Cache the results from AD, so that subsequent calls are faster. Enabled by default.
206
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
207
     * @param string $indexBy Attribute to use as an index.
208
     * @return array
209
     */
210
    public function getGroups($cached = true, $attributes = [], $indexBy = 'dn')
211
    {
212
        $searchLocations = $this->config()->groups_search_locations ?: [null];
213
        $cache = self::get_cache();
214
        $results = $cache->load('groups' . md5(implode('', array_merge($searchLocations, $attributes))));
215
216
        if (!$results || !$cached) {
217
            $results = [];
218 View Code Duplication
            foreach ($searchLocations as $searchLocation) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
219
                $records = $this->gateway->getGroups($searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
220
                if (!$records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
221
                    continue;
222
                }
223
224
                foreach ($records as $record) {
225
                    $results[$record[$indexBy]] = $record;
226
                }
227
            }
228
229
            $cache->save($results);
230
        }
231
232
        return $results;
233
    }
234
235
    /**
236
     * Return all member groups (and members of those, recursively) underneath a specific group DN.
237
     * Note that these get cached in-memory per-request for performance to avoid re-querying for the same results.
238
     *
239
     * @param string $dn
240
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
241
     * @return array
242
     */
243
    public function getNestedGroups($dn, $attributes = [])
244
    {
245
        if (isset(self::$_cache_nested_groups[$dn])) {
246
            return self::$_cache_nested_groups[$dn];
247
        }
248
249
        $searchLocations = $this->config()->groups_search_locations ?: [null];
250
        $results = [];
251 View Code Duplication
        foreach ($searchLocations as $searchLocation) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
252
            $records = $this->gateway->getNestedGroups($dn, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
253
            foreach ($records as $record) {
254
                $results[$record['dn']] = $record;
255
            }
256
        }
257
258
        self::$_cache_nested_groups[$dn] = $results;
259
        return $results;
260
    }
261
262
    /**
263
     * Get a particular AD group's data given a GUID.
264
     *
265
     * @param string $guid
266
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
267
     * @return array
268
     */
269 View Code Duplication
    public function getGroupByGUID($guid, $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...
270
    {
271
        $searchLocations = $this->config()->groups_search_locations ?: [null];
272
        foreach ($searchLocations as $searchLocation) {
273
            $records = $this->gateway->getGroupByGUID($guid, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
274
            if ($records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
275
                return $records[0];
276
            }
277
        }
278
    }
279
280
    /**
281
     * Get a particular AD group's data given a DN.
282
     *
283
     * @param string $dn
284
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
285
     * @return array
286
     */
287 View Code Duplication
    public function getGroupByDN($dn, $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...
288
    {
289
        $searchLocations = $this->config()->groups_search_locations ?: [null];
290
        foreach ($searchLocations as $searchLocation) {
291
            $records = $this->gateway->getGroupByDN($dn, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
292
            if ($records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
293
                return $records[0];
294
            }
295
        }
296
    }
297
298
    /**
299
     * Return all AD users in configured search locations, including all users in nested groups.
300
     * Uses users_search_locations if defined, otherwise falls back to NULL, which tells LDAPGateway
301
     * to use the default baseDn defined in the connection.
302
     *
303
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
304
     * @return array
305
     */
306
    public function getUsers($attributes = [])
307
    {
308
        $searchLocations = $this->config()->users_search_locations ?: [null];
309
        $results = [];
310
311 View Code Duplication
        foreach ($searchLocations as $searchLocation) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
312
            $records = $this->gateway->getUsers($searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
313
            if (!$records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
314
                continue;
315
            }
316
317
            foreach ($records as $record) {
318
                $results[$record['objectguid']] = $record;
319
            }
320
        }
321
322
        return $results;
323
    }
324
325
    /**
326
     * Get a specific AD user's data given a GUID.
327
     *
328
     * @param string $guid
329
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
330
     * @return array
331
     */
332 View Code Duplication
    public function getUserByGUID($guid, $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...
333
    {
334
        $searchLocations = $this->config()->users_search_locations ?: [null];
335
        foreach ($searchLocations as $searchLocation) {
336
            $records = $this->gateway->getUserByGUID($guid, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
337
            if ($records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
338
                return $records[0];
339
            }
340
        }
341
    }
342
343
    /**
344
     * Get a specific AD user's data given a DN.
345
     *
346
     * @param string $dn
347
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
348
     *
349
     * @return array
350
     */
351 View Code Duplication
    public function getUserByDN($dn, $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...
352
    {
353
        $searchLocations = $this->config()->users_search_locations ?: [null];
354
        foreach ($searchLocations as $searchLocation) {
355
            $records = $this->gateway->getUserByDN($dn, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
356
            if ($records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
357
                return $records[0];
358
            }
359
        }
360
    }
361
362
    /**
363
     * Get a specific user's data given an email.
364
     *
365
     * @param string $email
366
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
367
     * @return array
368
     */
369 View Code Duplication
    public function getUserByEmail($email, $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...
370
    {
371
        $searchLocations = $this->config()->users_search_locations ?: [null];
372
        foreach ($searchLocations as $searchLocation) {
373
            $records = $this->gateway->getUserByEmail($email, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
374
            if ($records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
375
                return $records[0];
376
            }
377
        }
378
    }
379
380
    /**
381
     * Get a specific user's data given a username.
382
     *
383
     * @param string $username
384
     * @param array $attributes List of specific AD attributes to return. Empty array means return everything.
385
     * @return array
386
     */
387 View Code Duplication
    public function getUserByUsername($username, $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...
388
    {
389
        $searchLocations = $this->config()->users_search_locations ?: [null];
390
        foreach ($searchLocations as $searchLocation) {
391
            $records = $this->gateway->getUserByUsername($username, $searchLocation, Zend\Ldap\Ldap::SEARCH_SCOPE_SUB, $attributes);
392
            if ($records) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $records of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
393
                return $records[0];
394
            }
395
        }
396
    }
397
398
    /**
399
     * Get a username for an email.
400
     *
401
     * @param string $email
402
     * @return string|null
403
     */
404
    public function getUsernameByEmail($email)
405
    {
406
        $data = $this->getUserByEmail($email);
407
        if (empty($data)) {
408
            return null;
409
        }
410
411
        return $this->gateway->getCanonicalUsername($data);
412
    }
413
414
    /**
415
     * Given a group DN, get the group membership data in LDAP.
416
     *
417
     * @param string $dn
418
     * @return array
419
     */
420
    public function getLDAPGroupMembers($dn)
421
    {
422
        $groupObj = Group::get()->filter('DN', $dn)->first();
423
        $groupData = $this->getGroupByGUID($groupObj->GUID);
424
        $members = !empty($groupData['member']) ? $groupData['member'] : [];
425
        // If a user belongs to a single group, this comes through as a string.
426
        // Normalise to a array so it's consistent.
427
        if ($members && is_string($members)) {
428
            $members = [$members];
429
        }
430
431
        return $members;
432
    }
433
434
    /**
435
     * Update the current Member record with data from LDAP.
436
     *
437
     * It's allowed to pass an unwritten Member record here, because it's not always possible to satisfy
438
     * field constraints without importing data from LDAP (for example if the application requires Username
439
     * through a Validator). Even though unwritten, it still must have the GUID set.
440
     *
441
     * Constraints:
442
     * - GUID of the member must have already been set, for integrity reasons we don't allow it to change here.
443
     *
444
     * @param Member
445
     * @param array|null $data If passed, this is pre-existing AD attribute data to update the Member with.
446
     *            If not given, the data will be looked up by the user's GUID.
447
     * @return bool
448
     */
449
    public function updateMemberFromLDAP(Member $member, $data = null)
450
    {
451
        if (!$this->enabled()) {
452
            return false;
453
        }
454
455
        if (!$member->GUID) {
456
            SS_Log::log(sprintf('Cannot update Member ID %s, GUID not set', $member->ID), SS_Log::WARN);
457
            return false;
458
        }
459
460
        if (!$data) {
461
            $data = $this->getUserByGUID($member->GUID);
462
            if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
463
                SS_Log::log(sprintf('Could not retrieve data for user. GUID: %s', $member->GUID), SS_Log::WARN);
464
                return false;
465
            }
466
        }
467
468
        $member->IsExpired = ($data['useraccountcontrol'] & 2) == 2;
469
        $member->LastSynced = (string)SS_Datetime::now();
470
471
        foreach ($member->config()->ldap_field_mappings as $attribute => $field) {
472
            if (!isset($data[$attribute])) {
473
                SS_Log::log(sprintf(
474
                    'Attribute %s configured in Member.ldap_field_mappings, but no available attribute in AD data (GUID: %s, Member ID: %s)',
475
                    $attribute,
476
                    $data['objectguid'],
477
                    $member->ID
478
                ), SS_Log::NOTICE);
479
480
                continue;
481
            }
482
483
            if ($attribute == 'thumbnailphoto') {
484
                $imageClass = $member->getRelationClass($field);
485
                if ($imageClass !== 'Image' && !is_subclass_of($imageClass, 'Image')) {
486
                    SS_Log::log(sprintf(
487
                        'Member field %s configured for thumbnailphoto AD attribute, but it isn\'t a valid relation to an Image class',
488
                        $field
489
                    ), SS_Log::WARN);
490
491
                    continue;
492
                }
493
494
                $filename = sprintf('thumbnailphoto-%s.jpg', $data['samaccountname']);
495
                $path = ASSETS_DIR . '/' . $member->config()->ldap_thumbnail_path;
496
                $absPath = BASE_PATH . '/' . $path;
497
                if (!file_exists($absPath)) {
498
                    Filesystem::makeFolder($absPath);
499
                }
500
501
                // remove existing record if it exists
502
                $existingObj = $member->getComponent($field);
503
                if ($existingObj && $existingObj->exists()) {
504
                    $existingObj->delete();
505
                }
506
507
                // The image data is provided in raw binary.
508
                file_put_contents($absPath . '/' . $filename, $data[$attribute]);
509
                $record = new $imageClass();
510
                $record->Name = $filename;
511
                $record->Filename = $path . '/' . $filename;
512
                $record->write();
513
514
                $relationField = $field . 'ID';
515
                $member->{$relationField} = $record->ID;
516
            } else {
517
                $member->$field = $data[$attribute];
518
            }
519
        }
520
521
        // if a default group was configured, ensure the user is in that group
522
        if ($this->config()->default_group) {
523
            $group = Group::get()->filter('Code', $this->config()->default_group)->limit(1)->first();
524
            if (!($group && $group->exists())) {
525
                SS_Log::log(
526
                    sprintf(
527
                        'LDAPService.default_group misconfiguration! There is no such group with Code = \'%s\'',
528
                        $this->config()->default_group
529
                    ),
530
                    SS_Log::WARN
531
                );
532
            } else {
533
                $group->Members()->add($member, [
534
                    'IsImportedFromLDAP' => '1'
535
                ]);
536
            }
537
        }
538
539
        // this is to keep track of which groups the user gets mapped to
540
        // and we'll use that later to remove them from any groups that they're no longer mapped to
541
        $mappedGroupIDs = [];
542
543
        // Member must have an ID before manipulating Groups, otherwise they will not be added correctly.
544
        // However we cannot do a full ->write before the groups are associated, because this will upsync
545
        // the Member, in effect deleting all their LDAP group associations!
546
        $member->writeWithoutSync();
547
548
        // ensure the user is in any mapped groups
549
        if (isset($data['memberof'])) {
550
            $ldapGroups = is_array($data['memberof']) ? $data['memberof'] : [$data['memberof']];
551
            foreach ($ldapGroups as $groupDN) {
552
                foreach (LDAPGroupMapping::get() as $mapping) {
553
                    if (!$mapping->DN) {
554
                        SS_Log::log(
555
                            sprintf(
556
                                'LDAPGroupMapping ID %s is missing DN field. Skipping',
557
                                $mapping->ID
558
                            ),
559
                            SS_Log::WARN
560
                        );
561
                        continue;
562
                    }
563
564
                    // the user is a direct member of group with a mapping, add them to the SS group.
565 View Code Duplication
                    if ($mapping->DN == $groupDN) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
566
                        $group = $mapping->Group();
567
                        if ($group && $group->exists()) {
568
                            $group->Members()->add($member, [
569
                                'IsImportedFromLDAP' => '1'
570
                            ]);
571
                            $mappedGroupIDs[] = $mapping->GroupID;
572
                        }
573
                    }
574
575
                    // the user *might* be a member of a nested group provided the scope of the mapping
576
                    // is to include the entire subtree. Check all those mappings and find the LDAP child groups
577
                    // to see if they are a member of one of those. If they are, add them to the SS group
578
                    if ($mapping->Scope == 'Subtree') {
579
                        $childGroups = $this->getNestedGroups($mapping->DN, ['dn']);
580
                        if (!$childGroups) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $childGroups of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
581
                            continue;
582
                        }
583
584
                        foreach ($childGroups as $childGroupDN => $childGroupRecord) {
585 View Code Duplication
                            if ($childGroupDN == $groupDN) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
586
                                $group = $mapping->Group();
587
                                if ($group && $group->exists()) {
588
                                    $group->Members()->add($member, [
589
                                        'IsImportedFromLDAP' => '1'
590
                                    ]);
591
                                    $mappedGroupIDs[] = $mapping->GroupID;
592
                                }
593
                            }
594
                        }
595
                    }
596
                }
597
            }
598
        }
599
600
        // remove the user from any previously mapped groups, where the mapping has since been removed
601
        $groupRecords = DB::query(sprintf('SELECT "GroupID" FROM "Group_Members" WHERE "IsImportedFromLDAP" = 1 AND "MemberID" = %s', $member->ID));
602
        foreach ($groupRecords as $groupRecord) {
603
            if (!in_array($groupRecord['GroupID'], $mappedGroupIDs)) {
604
                $group = Group::get()->byId($groupRecord['GroupID']);
605
                // Some groups may no longer exist. SilverStripe does not clean up join tables.
606
                if ($group) {
607
                    $group->Members()->remove($member);
608
                }
609
            }
610
        }
611
        // This will throw an exception if there are two distinct GUIDs with the same email address.
612
        // We are happy with a raw 500 here at this stage.
613
        $member->write();
614
    }
615
616
    /**
617
     * Sync a specific Group by updating it with LDAP data.
618
     *
619
     * @param Group $group An existing Group or a new Group object
620
     * @param array $data LDAP group object data
621
     *
622
     * @return bool
623
     */
624
    public function updateGroupFromLDAP(Group $group, $data)
625
    {
626
        if (!$this->enabled()) {
627
            return false;
628
        }
629
630
        // Synchronise specific guaranteed fields.
631
        $group->Code = $data['samaccountname'];
632
        $group->Title = $data['samaccountname'];
633
        if (!empty($data['description'])) {
634
            $group->Description = $data['description'];
635
        }
636
        $group->DN = $data['dn'];
637
        $group->LastSynced = (string)SS_Datetime::now();
638
        $group->write();
639
640
        // Mappings on this group are automatically maintained to contain just the group's DN.
641
        // First, scan through existing mappings and remove ones that are not matching (in case the group moved).
642
        $hasCorrectMapping = false;
643
        foreach ($group->LDAPGroupMappings() as $mapping) {
644
            if ($mapping->DN === $data['dn']) {
645
                // This is the correct mapping we want to retain.
646
                $hasCorrectMapping = true;
647
            } else {
648
                $mapping->delete();
649
            }
650
        }
651
652
        // Second, if the main mapping was not found, add it in.
653
        if (!$hasCorrectMapping) {
654
            $mapping = new LDAPGroupMapping();
655
            $mapping->DN = $data['dn'];
0 ignored issues
show
Documentation introduced by
The property DN does not exist on object<LDAPGroupMapping>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
656
            $mapping->write();
657
            $group->LDAPGroupMappings()->add($mapping);
658
        }
659
    }
660
661
    /**
662
     * Creates a new LDAP user from the passed Member record.
663
     * Note that the Member record must have a non-empty Username field for this to work.
664
     *
665
     * @param Member $member
666
     */
667
    public function createLDAPUser(Member $member)
668
    {
669
        if (!$this->enabled()) {
670
            return;
671
        }
672
        if (empty($member->Username)) {
673
            throw new ValidationException('Member missing Username. Cannot create LDAP user');
674
        }
675
        if (!$this->config()->new_users_dn) {
676
            throw new Exception('LDAPService::new_users_dn must be configured to create LDAP users');
677
        }
678
679
        // Normalise username to lowercase to ensure we don't have duplicates of different cases
680
        $member->Username = strtolower($member->Username);
681
682
        // Create user in LDAP using available information.
683
        $dn = sprintf('CN=%s,%s', $member->Username, $this->config()->new_users_dn);
684
685
        try {
686
            $this->add($dn, [
687
                'objectclass' => 'user',
688
                'cn' => $member->Username,
689
                'accountexpires' => '9223372036854775807',
690
                'useraccountcontrol' => '66048',
691
                'userprincipalname' => sprintf(
692
                    '%s@%s',
693
                    $member->Username,
694
                    $this->gateway->config()->options['accountDomainName']
695
                ),
696
            ]);
697
        } catch (\Exception $e) {
698
            throw new ValidationException('LDAP synchronisation failure: '.$e->getMessage());
699
        }
700
701
        $user = $this->getUserByUsername($member->Username);
702
        if (empty($user['objectguid'])) {
703
            throw new ValidationException('LDAP synchronisation failure: user missing GUID');
704
        }
705
706
        // Creation was successful, mark the user as LDAP managed by setting the GUID.
707
        $member->GUID = $user['objectguid'];
708
    }
709
710
    /**
711
     * Creates a new LDAP group from the passed Group record.
712
     *
713
     * @param Group $group
714
     * @throws ValidationException
715
     */
716
    public function createLDAPGroup(Group $group) {
717
        if (!$this->enabled()) {
718
            return;
719
        }
720
        if (empty($group->Title)) {
721
            throw new ValidationException('Group missing Title. Cannot create LDAP group');
722
        }
723
        if (!$this->config()->new_groups_dn) {
724
            throw new Exception('LDAPService::new_groups_dn must be configured to create LDAP groups');
725
        }
726
727
        // LDAP isn't really meant to distinguish between a Title and Code. Squash them.
728
        $group->Code = $group->Title;
729
730
        $dn = sprintf('CN=%s,%s', $group->Title, $this->config()->new_groups_dn);
731
        try {
732
            $this->add($dn, [
733
                'objectclass' => 'group',
734
                'cn' => $group->Title,
735
                'name' => $group->Title,
736
                'samaccountname' => $group->Title,
737
                'description' => $group->Description,
738
                'distinguishedname' => $dn
739
            ]);
740
        } catch (\Exception $e) {
741
            throw new \ValidationException('LDAP group creation failure: ' . $e->getMessage());
742
        }
743
744
        $data = $this->getGroupByDN($dn);
745
        if (empty($data['objectguid'])) {
746
            throw new \ValidationException(
747
                new \ValidationResult(
748
                    false,
749
                    'LDAP group creation failure: group might have been created in LDAP. GUID is missing.'
750
                )
751
            );
752
        }
753
754
        // Creation was successful, mark the group as LDAP managed by setting the GUID.
755
        $group->GUID = $data['objectguid'];
756
        $group->DN = $data['dn'];
757
    }
758
759
    /**
760
     * Update the Member data back to the corresponding LDAP user object.
761
     *
762
     * @param Member $member
763
     * @throws ValidationException
764
     */
765
    public function updateLDAPFromMember(Member $member)
766
    {
767
        if (!$this->enabled()) {
768
            return;
769
        }
770
        if (!$member->GUID) {
771
            throw new ValidationException('Member missing GUID. Cannot update LDAP user');
772
        }
773
774
        $data = $this->getUserByGUID($member->GUID);
775
        if (empty($data['objectguid'])) {
776
            throw new ValidationException('LDAP synchronisation failure: user missing GUID');
777
        }
778
779
        if (empty($member->Username)) {
780
            throw new ValidationException('Member missing Username. Cannot update LDAP user');
781
        }
782
783
        $dn = $data['distinguishedname'];
784
785
        // Normalise username to lowercase to ensure we don't have duplicates of different cases
786
        $member->Username = strtolower($member->Username);
787
788
        try {
789
            // If the common name (cn) has changed, we need to ensure they've been moved
790
            // to the new DN, to avoid any clashes between user objects.
791
            if ($data['cn'] != $member->Username) {
792
                $newDn = sprintf('CN=%s,%s', $member->Username, preg_replace('/^CN=(.+?),/', '', $dn));
793
                $this->move($dn, $newDn);
794
                $dn = $newDn;
795
            }
796
        } catch (\Exception $e) {
797
            throw new ValidationException('LDAP move failure: '.$e->getMessage());
798
        }
799
800
        try {
801
            $attributes = [
802
                'displayname' => sprintf('%s %s', $member->FirstName, $member->Surname),
803
                'name' => sprintf('%s %s', $member->FirstName, $member->Surname),
804
                'userprincipalname' => sprintf(
805
                    '%s@%s',
806
                    $member->Username,
807
                    $this->gateway->config()->options['accountDomainName']
808
                ),
809
            ];
810
            foreach ($member->config()->ldap_field_mappings as $attribute => $field) {
811
                $relationClass = $member->getRelationClass($field);
812
                if ($relationClass) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
813
                    // todo no support for writing back relations yet.
814
                } else {
815
                    $attributes[$attribute] = $member->$field;
816
                }
817
            }
818
819
            $this->update($dn, $attributes);
820
        } catch (\Exception $e) {
821
            throw new ValidationException('LDAP synchronisation failure: '.$e->getMessage());
822
        }
823
    }
824
825
    /**
826
     * Ensure the user belongs to the correct groups in LDAP from their membership
827
     * to local LDAP mapped SilverStripe groups.
828
     *
829
     * This also removes them from LDAP groups if they've been taken out of one.
830
     * It will not affect group membership of non-mapped groups, so it will
831
     * not touch such internal AD groups like "Domain Users".
832
     *
833
     * @param Member $member
834
     */
835
    public function updateLDAPGroupsForMember(Member $member)
836
    {
837
        if (!$this->enabled()) {
838
            return;
839
        }
840
        if (!$member->GUID) {
841
            throw new ValidationException('Member missing GUID. Cannot update LDAP user');
842
        }
843
844
        $addGroups = [];
845
        $removeGroups = [];
846
847
        $user = $this->getUserByGUID($member->GUID);
848
        if (empty($user['objectguid'])) {
849
            throw new ValidationException('LDAP update failure: user missing GUID');
850
        }
851
852
        // If a user belongs to a single group, this comes through as a string.
853
        // Normalise to a array so it's consistent.
854
        $existingGroups = !empty($user['memberof']) ? $user['memberof'] : [];
855
        if ($existingGroups && is_string($existingGroups)) {
856
            $existingGroups = [$existingGroups];
857
        }
858
859
        foreach ($member->Groups() as $group) {
860
            if (!$group->GUID) {
861
                continue;
862
            }
863
864
            // mark this group as something we need to ensure the user belongs to in LDAP.
865
            $addGroups[] = $group->DN;
866
        }
867
868
        // Which existing LDAP groups are not in the add groups? We'll check these groups to
869
        // see if the user should be removed from any of them.
870
        $remainingGroups = array_diff($existingGroups, $addGroups);
871
872
        foreach ($remainingGroups as $groupDn) {
873
            // We only want to be removing groups we have a local Group mapped to. Removing
874
            // membership for anything else would be bad!
875
            $group = Group::get()->filter('DN', $groupDn)->first();
876
            if (!$group || !$group->exists()) {
877
                continue;
878
            }
879
880
            // this group should be removed from the user's memberof attribute, as it's been removed.
881
            $removeGroups[] = $groupDn;
882
        }
883
884
        // go through the groups we want the user to be in and ensure they're in them.
885
        foreach ($addGroups as $groupDn) {
886
            $this->addLDAPUserToGroup($user['distinguishedname'], $groupDn);
887
        }
888
889
        // go through the groups we _don't_ want the user to be in and ensure they're taken out of them.
890
        foreach ($removeGroups as $groupDn) {
891
            $members = $this->getLDAPGroupMembers($groupDn);
892
893
            // remove the user from the members data.
894
            if (in_array($user['distinguishedname'], $members)) {
895
                foreach ($members as $i => $dn) {
896
                    if ($dn == $user['distinguishedname']) {
897
                        unset($members[$i]);
898
                    }
899
                }
900
            }
901
902
            try {
903
                $this->update($groupDn, ['member' => $members]);
904
            } catch (\Exception $e) {
905
                throw new ValidationException('LDAP group membership remove failure: '.$e->getMessage());
906
            }
907
        }
908
    }
909
910
    /**
911
     * Add LDAP user by DN to LDAP group.
912
     *
913
     * @param string $userDn
914
     * @param string $groupDn
915
     * @throws \Exception
916
     */
917
    public function addLDAPUserToGroup($userDn, $groupDn) {
918
        $members = $this->getLDAPGroupMembers($groupDn);
919
920
        // this user is already in the group, no need to do anything.
921
        if (in_array($userDn, $members)) {
922
            return;
923
        }
924
925
        $members[] = $userDn;
926
927
        try {
928
            $this->update($groupDn, ['member' => $members]);
929
        } catch (\Exception $e) {
930
            throw new ValidationException('LDAP group membership add failure: '.$e->getMessage());
931
        }
932
    }
933
934
    /**
935
     * Change a members password on the AD. Works with ActiveDirectory compatible services that saves the
936
     * password in the `unicodePwd` attribute.
937
     *
938
     * @todo Use the Zend\Ldap\Attribute::setPassword functionality to create a password in
939
     * an abstract way, so it works on other LDAP directories, not just Active Directory.
940
     *
941
     * Ensure that the LDAP bind:ed user can change passwords and that the connection is secure.
942
     *
943
     * @param Member $member
944
     * @param string $password
945
     * @param string|null $oldPassword Supply old password to perform a password change (as opposed to password reset)
946
     * @return ValidationResult
947
     * @throws Exception
948
     */
949
    public function setPassword(Member $member, $password, $oldPassword = null)
950
    {
951
        $validationResult = ValidationResult::create(true);
952
953
        $this->extend('onBeforeSetPassword', $member, $password, $validationResult);
954
955
        if (!$member->GUID) {
956
            SS_Log::log(sprintf('Cannot update Member ID %s, GUID not set', $member->ID), SS_Log::WARN);
957
            $validationResult->error(_t('LDAPAuthenticator.NOUSER', 'Your account hasn\'t been setup properly, please contact an administrator.'));
958
            return $validationResult;
959
        }
960
961
        $userData = $this->getUserByGUID($member->GUID);
962
        if (empty($userData['distinguishedname'])) {
963
            $validationResult->error(_t('LDAPAuthenticator.NOUSER', 'Your account hasn\'t been setup properly, please contact an administrator.'));
964
            return $validationResult;
965
        }
966
967
        try {
968
            if (!empty($oldPassword)) {
969
                $this->gateway->changePassword($userData['distinguishedname'], $password, $oldPassword);
970
            } else if ($this->config()->password_history_workaround) {
971
                $this->passwordHistoryWorkaround($userData['distinguishedname'], $password);
972
            } else {
973
                $this->gateway->resetPassword($userData['distinguishedname'], $password);
974
            }
975
            $this->extend('onAfterSetPassword', $member, $password, $validationResult);
976
        } catch (Exception $e) {
977
            $validationResult->error($e->getMessage());
978
        }
979
980
        return $validationResult;
981
    }
982
983
    /**
984
     * Delete an LDAP user mapped to the Member record
985
     * @param Member $member
986
     */
987
    public function deleteLDAPMember(Member $member) {
988
        if (!$this->enabled()) {
989
            return;
990
        }
991
        if (!$member->GUID) {
992
            throw new ValidationException('Member missing GUID. Cannot delete LDAP user');
993
        }
994
        $data = $this->getUserByGUID($member->GUID);
995
        if (empty($data['distinguishedname'])) {
996
            throw new ValidationException('LDAP delete failure: could not find distinguishedname attribute');
997
        }
998
999
        try {
1000
            $this->delete($data['distinguishedname']);
1001
        } catch (\Exception $e) {
1002
            throw new ValidationException('LDAP delete user failed: '.$e->getMessage());
1003
        }
1004
    }
1005
1006
    /**
1007
     * A simple proxy to LDAP update operation.
1008
     *
1009
     * @param string $dn Location to add the entry at.
1010
     * @param array $attributes A simple associative array of attributes.
1011
     */
1012
    public function update($dn, array $attributes)
1013
    {
1014
        $this->gateway->update($dn, $attributes);
1015
    }
1016
1017
    /**
1018
     * A simple proxy to LDAP delete operation.
1019
     *
1020
     * @param string $dn Location of object to delete
1021
     * @param bool $recursively Recursively delete nested objects?
1022
     */
1023
    public function delete($dn, $recursively = false)
1024
    {
1025
        $this->gateway->delete($dn, $recursively);
1026
    }
1027
1028
    /**
1029
     * A simple proxy to LDAP copy/delete operation.
1030
     *
1031
     * @param string $fromDn
1032
     * @param string $toDn
1033
     * @param bool $recursively Recursively move nested objects?
1034
     */
1035
    public function move($fromDn, $toDn, $recursively = false)
1036
    {
1037
        $this->gateway->move($fromDn, $toDn, $recursively);
1038
    }
1039
1040
    /**
1041
     * A simple proxy to LDAP add operation.
1042
     *
1043
     * @param string $dn Location to add the entry at.
1044
     * @param array $attributes A simple associative array of attributes.
1045
     */
1046
    public function add($dn, array $attributes)
1047
    {
1048
        $this->gateway->add($dn, $attributes);
1049
    }
1050
1051
    /**
1052
     * @param string $dn Distinguished name of the user
1053
     * @param string $password New password.
1054
     * @throws Exception
1055
     */
1056
    private function passwordHistoryWorkaround($dn, $password) {
1057
        $generator = new RandomGenerator();
1058
        // 'Aa1' is there to satisfy the complexity criterion.
1059
        $tempPassword = sprintf('Aa1%s', substr($generator->randomToken('sha1'), 0, 21));
1060
        $this->gateway->resetPassword($dn, $tempPassword);
1061
        $this->gateway->changePassword($dn, $password, $tempPassword);
1062
    }
1063
1064
}
1065