Completed
Push — master ( bce1be...f50252 )
by Christopher
02:19
created

Connection::changePasswordAsUser()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 3
1
<?php
2
/**
3
 * @link      https://github.com/chrmorandi/yii2-ldap for the canonical source repository
4
 * @package   yii2-ldap
5
 * @author    Christopher Mota <[email protected]>
6
 * @license   MIT License - view the LICENSE file that was distributed with this source code.
7
 */
8
9
namespace chrmorandi\ldap;
10
11
use Yii;
12
use yii\base\Component;
13
use yii\caching\Cache;
14
15
/**
16
 * @property resource $resource
17
 * @property boolean  $bount
18
 * @property int      $errNo Error number of the last command
19
 * @property string   $lastError Error message of the last command
20
 *
21
 * @author Christopher Mota <[email protected]>
22
 * @since 1.0
23
 */
24
class Connection extends Component
25
{
26
    /**
27
     * LDAP protocol string.
28
     * @var string
29
     */
30
    const PROTOCOL = 'ldap://';
31
32
    /**
33
     * LDAP port number.
34
     * @var string
35
     */
36
    const PORT = '389';
37
38
    /**
39
     * @event Event an event that is triggered after a DB connection is established
40
     */
41
    const EVENT_AFTER_OPEN = 'afterOpen';
42
43
    /**
44
     * @var string the LDAP base dn.
45
     */
46
    public $baseDn;
47
48
    /**
49
     * https://msdn.microsoft.com/en-us/library/ms677913(v=vs.85).aspx
50
     * @var bool the integer to instruct the LDAP connection whether or not to follow referrals.
51
     */
52
    public $followReferrals = false;
53
54
    /**
55
     * @var string The LDAP port to use when connecting to the domain controllers.
56
     */
57
    public $port = self::PORT;
58
59
    /**
60
     * @var bool Determines whether or not to use TLS with the current LDAP connection.
61
     */
62
    public $useTLS = true;
63
64
    /**
65
     * @var array the domain controllers to connect to.
66
     */
67
    public $dc = [];
68
    
69
    /**
70
     * @var string the username for establishing LDAP connection. Defaults to `null` meaning no username to use.
71
     */
72
    public $username;
73
74
    /**
75
     * @var string the password for establishing DB connection. Defaults to `null` meaning no password to use.
76
     */
77
    public $password;
78
    
79
    /**
80
     * @var int The page size for the paging operation.
81
     */
82
    public $pageSize = -1;
83
    
84
    /**
85
     * @var integer zero-based offset from where the records are to be returned. If not set or
86
     * less than 1, it means not filter values.
87
     */
88
    public $offset = -1;
89
    
90
    /**
91
     * @var boolean whether to enable caching.
92
     * Note that in order to enable query caching, a valid cache component as specified
93
     * by [[cache]] must be enabled and [[enableCache]] must be set true.
94
     * Also, only the results of the queries enclosed within [[cache()]] will be cached.
95
     * @see cacheDuration
96
     * @see cache
97
     */
98
    public $enableCache = true;
99
    
100
    /**
101
     * @var integer number of seconds that table metadata can remain valid in cache.
102
     * Use 0 to indicate that the cached data will never expire.
103
     * @see enableCache
104
     */
105
    public $cacheDuration = 3600;
106
107
    /**
108
     * @var Cache|string the cache object or the ID of the cache application component that
109
     * is used to cache result query.
110
     * @see enableCache
111
     */
112
    public $cache = 'cache';
113
    
114
    /**
115
     * @var string the attribute for authentication
116
     */    
117
    public $loginAttribute = "sAMAccountName";
118
    
119
    /**
120
     * @var bool stores the bool whether or not the current connection is bound.
121
     */
122
    protected $_bound = false;
123
124
    /**
125
     * @var resource|false
126
     */
127
    protected $resource;
128
    
129
    /**
130
     *
131
     * @var string 
132
     */
133
    protected $userDN;
134
    
135
    # Create AD password (Microsoft Active Directory password format)
136
    protected static function encodePassword($password) {
137
        $password = "\"" . $password . "\"";
138
        $adpassword = mb_convert_encoding($password, "UTF-16LE", "UTF-8");
139
        return $adpassword;
140
    }
141
142
    /**
143
     * Returns the current query cache information.
144
     * This method is used internally by [[Command]].
145
     * @param integer $duration the preferred caching duration. If null, it will be ignored.
146
     * @param \yii\caching\Dependency $dependency the preferred caching dependency. If null, it will be ignored.
147
     * @return array the current query cache information, or null if query cache is not enabled.
148
     * @internal
149
     */
150
    public function getCacheInfo($duration = 3600, $dependency = null)
151
    {
152
        if (!$this->enableCache) {
153
            return null;
154
        }
155
156
        if ($duration === 0 || $duration > 0) {
157
            if (is_string($this->cache) && Yii::$app) {
158
                $cache = Yii::$app->get($this->cache, false);
159
            } else {
160
                $cache = $this->cache;
161
            }
162
            if ($cache instanceof Cache) {
163
                return [$cache, $duration, $dependency];
164
            }
165
        }
166
167
        return null;
168
    }
169
    
170
    /**
171
     * Invalidates the cached data that are associated with any of the specified [[tags]] in this connection.
172
     * @param string|array $tags
173
     */
174
    public function clearCache($tags)
175
    {
176
        $cache = Yii::$app->get($this->cache, false);
0 ignored issues
show
Bug introduced by
It seems like $this->cache can also be of type object<yii\caching\Cache>; however, yii\di\ServiceLocator::get() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
177
        \yii\caching\TagDependency::invalidate($cache, $tags);
178
    }
179
180
    /**
181
     * Connects and Binds to the Domain Controller with a administrator credentials.
182
     * @return void
183
     */
184
    public function open($anonymous = false)
185
    {
186
        $token = 'Opening LDAP connection: ' . LdapUtils::recursive_implode($this->dc, ' or ');
187
        Yii::info($token, __METHOD__);
188
        Yii::beginProfile($token, __METHOD__);
189
        // Connect to the LDAP server.
190
        $this->connect($this->dc, $this->port);
191
        Yii::endProfile($token, __METHOD__);
192
193
        try {
194
            if ($anonymous) {
195
                $this->_bound = ldap_bind($this->resource);
196
            } else {
197
                $this->_bound = ldap_bind($this->resource, $this->username, $this->password);
198
            }
199
        } catch (\Exception $e) {
200
            throw new \Exception('Invalid credential for user manager in ldap.', 0);
201
        }
202
    }
203
204
    /**
205
     * Connection.
206
     * @param string|array $hostname
207
     * @param type $port
208
     * @return void
209
     */
210
    protected function connect($hostname = [], $port = '389')
211
    {
212
        if (is_array($hostname)) {
213
            $hostname = self::PROTOCOL.implode(' '.self::PROTOCOL, $hostname);
214
        }
215
        
216
        $this->close();
217
        $this->resource = ldap_connect($hostname, $port);
218
219
        // Set the LDAP options.     
220
        $this->setOption(LDAP_OPT_PROTOCOL_VERSION, 3);
221
        $this->setOption(LDAP_OPT_REFERRALS, $this->followReferrals);
222
        if ($this->useTLS) {
223
            $this->startTLS();
224
        }
225
226
        $this->trigger(self::EVENT_AFTER_OPEN);
227
    }
228
    
229
    /**
230
     * Authenticate user
231
     * @param string $username
232
     * @param string $password
233
     * @return int indicate occurrence of error. 
234
     */
235
    public function auth($username, $password)
236
    {
237
        // Open connection with manager
238
        $this->open();
239
        
240
        # Search for user and get user DN
241
        $searchResult = ldap_search($this->resource, $this->baseDn, "(&(objectClass=person)($this->loginAttribute=$username))", [$this->loginAttribute]);
242
        $entry = $this->getFirstEntry($searchResult);
243
        if($entry) {
244
            $this->userDN = $this->getDn($entry);        
245
        } else {
246
            $this->userDN = null;
247
        }
248
249
        // Connect to the LDAP server.
250
        $this->connect($this->dc, $this->port);
251
252
        // Authenticate user
253
        return ldap_bind($this->resource, $this->userDN, $password);
254
    }
255
    
256
    /**
257
     * Change the password of the current user. This must be performed over TLS.
258
     * @param string $username User for change password
259
     * @param string $oldPassword The old password
260
     * @param string $newPassword The new password
261
     * @return bool return true if change password is success
262
     * @throws \Exception
263
     */
264
    public function changePasswordAsUser($username, $oldPassword, $newPassword)
265
    {        
266
        if (!$this->useTLS) {
267
            $message = 'TLS must be configured on your web server and enabled to change passwords.';
268
            throw new \Exception($message);
269
        }
270
271
        // Open connection with user
272
        if(!$this->auth($username, $oldPassword)){
273
            return false;
274
        }
275
        
276
        return $this->changePasswordAsManager($this->userDN, $newPassword);
277
    }
278
    
279
    /**
280
     * Change the password of the user as manager. This must be performed over TLS.
281
     * @param string $userDN User Distinguished Names (DN) for change password. Ex.: cn=admin,dc=example,dc=com
282
     * @param string $newPassword The new password
283
     * @return bool return true if change password is success
284
     * @throws \Exception
285
     */
286
    public function changePasswordAsManager($userDN, $newPassword)
287
    {        
288
        if (!$this->useTLS) {
289
            $message = 'TLS must be configured on your web server and enabled to change passwords.';
290
            throw new \Exception($message);
291
        }
292
        
293
        // Open connection with manager
294
        $this->open();
295
        
296
        // Replace passowrd attribute for AD
297
        // The AD password change procedure is modifying the attribute unicodePwd
298
        $modifications['unicodePwd'] = self::encodePassword($newPassword);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$modifications was never initialized. Although not strictly required by PHP, it is generally a good practice to add $modifications = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
299
        return ldap_mod_replace($this->resource, $userDN, $modifications);
300
    }
301
    
302
    /**
303
     * Closes the current connection.
304
     *
305
     * @return boolean
306
     */
307
    public function close()
308
    {
309
        if (is_resource($this->resource)) {
310
            ldap_close($this->resource);
311
        }
312
        return true;
313
    }
314
315
    /**
316
     * Execute ldap search like.
317
     *
318
     * http://php.net/manual/en/ref.ldap.php
319
     *
320
     * @param  string $function php LDAP function
321
     * @param  array $params params for execute ldap function
322
     * @return bool|DataReader
323
     */
324
    public function executeQuery($function, $params)
325
    {
326
        $this->open();
327
        $results = [];
328
        $cookie = '';        
329
        $token = $function . ' - params: ' . LdapUtils::recursive_implode($params, ';');
330
331
        Yii::info($token , 'chrmorandi\ldap\Connection::query');
332
       
333
        Yii::beginProfile($token, 'chrmorandi\ldap\Connection::query');
334
        do {
335
            if($this->pageSize > 0) {
336
                $this->setControlPagedResult($cookie);
337
            }
338
            
339
            // Run the search.
340
            $result = call_user_func($function, $this->resource, ...$params);
341
            
342
            if($this->pageSize > 0) {
343
                $this->setControlPagedResultResponse($result, $cookie);
344
            }
345
            
346
            //Collect each resource result
347
            $results[] = $result;            
348
        } while (!is_null($cookie) && !empty($cookie));
349
        Yii::endProfile($token, 'chrmorandi\ldap\Connection::query');
350
351
        return new DataReader($this, $results);
352
    }
353
    
354
    /**
355
     * Returns true/false if the current connection is bound.
356
     * @return bool
357
     */
358
    public function getBound()
359
    {
360
        return $this->_bound;
361
    }
362
    
363
    /**
364
     * Get the current resource of connection.
365
     * @return resource
366
     */
367
    public function getResource()
368
    {
369
        return $this->resource;
370
    }
371
    
372
    /**
373
     * Sorts an AD search result by the specified attribute.
374
     * @param resource $result
375
     * @param string   $attribute
376
     * @return bool
377
     */
378
    public function sort($result, $attribute)
379
    {
380
        return ldap_sort($this->resource, $result, $attribute);
381
    }
382
383
    /**
384
     * Adds an entry to the current connection.
385
     * @param string $dn
386
     * @param array  $entry
387
     * @return bool
388
     */
389
    public function add($dn, array $entry)
390
    {
391
        return ldap_add($this->resource, $dn, $entry);
392
    }
393
394
    /**
395
     * Deletes an entry on the current connection.
396
     * @param string $dn
397
     * @return bool
398
     */
399
    public function delete($dn)
400
    {
401
        return ldap_delete($this->resource, $dn);
402
    }
403
404
    /**
405
     * Modify the name of an entry on the current connection.
406
     *
407
     * @param string $dn
408
     * @param string $newRdn
409
     * @param string $newParent
410
     * @param bool   $deleteOldRdn
411
     * @return bool
412
     */
413
    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
414
    {
415
        return ldap_rename($this->resource, $dn, $newRdn, $newParent, $deleteOldRdn);
416
    }
417
418
    /**
419
     * Batch modifies an existing entry on the current connection.
420
     * The types of modifications:
421
     *      LDAP_MODIFY_BATCH_ADD - Each value specified through values is added.
422
     *      LDAP_MODIFY_BATCH_REMOVE - Each value specified through values is removed. 
423
     *          Any value of the attribute not contained in the values array will remain untouched.
424
     *      LDAP_MODIFY_BATCH_REMOVE_ALL - All values are removed from the attribute named by attrib.
425
     *      LDAP_MODIFY_BATCH_REPLACE - All current values are replaced by new one.
426
     * @param string $dn
427
     * @param array  $values array associative with three keys: "attrib", "modtype" and "values".
428
     * ```php
429
     * [
430
     *     "attrib"  => "attribute",
431
     *     "modtype" => LDAP_MODIFY_BATCH_ADD,
432
     *     "values"  => ["attribute value one"],
433
     * ],
434
     * ```
435
     * @return mixed
436
     */
437
    public function modify($dn, array $values)
438
    {
439
        $this->clearCache(DataReader::CACHE_TAG);
440
        return ldap_modify_batch($this->resource, $dn, $values);
441
    }    
442
    
443
    /**
444
     * Retrieve the entries from a search result.
445
     * @param resource $searchResult
446
     * @return array|boolean
447
     */
448
    public function getEntries($searchResult)
449
    {
450
        return ldap_get_entries($this->resource, $searchResult);
451
    }
452
    
453
    /**
454
     * Retrieves the number of entries from a search result.
455
     * @param resource $searchResult
456
     * @return int
457
     */
458
    public function countEntries($searchResult)
459
    {
460
        return ldap_count_entries($this->resource, $searchResult);
461
    }
462
463
    /**
464
     * Retrieves the first entry from a search result.
465
     * @param resource $searchResult
466
     * @return resource link identifier
467
     */
468
    public function getFirstEntry($searchResult)
469
    {
470
        return ldap_first_entry($this->resource, $searchResult);
471
    }
472
473
    /**
474
     * Retrieves the next entry from a search result.
475
     * @param resource $entry link identifier
476
     * @return resource
477
     */
478
    public function getNextEntry($entry)
479
    {
480
        return ldap_next_entry($this->resource, $entry);
481
    }
482
    
483
    /**
484
     * Retrieves the ldap first entry attribute.
485
     * @param resource $entry
486
     * @return string
487
     */
488
    public function getFirstAttribute($entry)
489
    {
490
        return ldap_first_attribute($this->resource, $entry);
491
    }
492
    
493
    /**
494
     * Retrieves the ldap next entry attribute.
495
     * @param resource $entry
496
     * @return string
497
     */
498
    public function getNextAttribute($entry)
499
    {
500
        return ldap_next_attribute($this->resource, $entry);
501
    }
502
503
    /**
504
     * Retrieves the ldap entry's attributes.
505
     * @param resource $entry
506
     * @return array
507
     */
508
    public function getAttributes($entry)
509
    {
510
        return ldap_get_attributes($this->resource, $entry);
511
    }
512
    
513
    /**
514
     * Retrieves all binary values from a result entry.
515
     * @param resource $entry link identifier
516
     * @param string $attribute name of attribute
517
     * @return array
518
     */
519
    public function getValuesLen($entry, $attribute)
520
    {
521
        return ldap_get_values_len($this->resource, $entry, $attribute);
522
    }
523
    
524
    /**
525
     * Retrieves the DN of a result entry.
526
     * @param resource $entry
527
     * @return string
528
     */
529
    public function getDn($entry)
530
    {
531
        return ldap_get_dn($this->resource, $entry);
532
    }
533
534
    /**
535
     * Free result memory.
536
     * @param resource $searchResult
537
     * @return bool
538
     */
539
    public function freeResult($searchResult)
540
    {
541
        return ldap_free_result($searchResult);
542
    }
543
544
    /**
545
     * Sets an option on the current connection.
546
     * @param int   $option
547
     * @param mixed $value
548
     * @return boolean
549
     */
550
    public function setOption($option, $value)
551
    {
552
        return ldap_set_option($this->resource, $option, $value);
553
    }
554
555
    /**
556
     * Starts a connection using TLS.
557
     * @return bool
558
     */
559
    public function startTLS()
560
    {
561
        return ldap_start_tls($this->resource);
562
    }
563
    
564
    /**
565
     * Send LDAP pagination control.
566
     * @param int    $pageSize
0 ignored issues
show
Bug introduced by
There is no parameter named $pageSize. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
567
     * @param bool   $isCritical
0 ignored issues
show
Bug introduced by
There is no parameter named $isCritical. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
568
     * @param string $cookie
569
     * @return bool
570
     */
571
    public function setControlPagedResult($cookie)
572
    {
573
        return ldap_control_paged_result($this->resource, $this->pageSize, false, $cookie);
574
    }
575
576
    /**
577
     * Retrieve a paginated result response.
578
     * @param resource $result
579
     * @param string $cookie
580
     * @return bool
581
     */
582
    public function setControlPagedResultResponse($result, &$cookie)
583
    {
584
        return ldap_control_paged_result_response($this->resource, $result, $cookie);
585
    }
586
       
587
    /**
588
     * Retrieve the last error on the current connection.
589
     * @return string
590
     */
591
    public function getLastError()
592
    {
593
        return ldap_error($this->resource);
594
    }
595
    
596
    /**
597
     * Returns the number of the last error on the current connection.
598
     * @return int
599
     */
600
    public function getErrNo()
601
    {
602
        return ldap_errno($this->resource);
603
    }
604
605
    /**
606
     * Returns the error string of the specified error number.
607
     * @param int $number
608
     * @return string
609
     */
610
    public function err2Str($number)
611
    {
612
        return ldap_err2str($number);
613
    }
614
}
615