Completed
Push — master ( 9a3205...820991 )
by Chad
02:38
created

LdapUserProvider::setRefreshRoles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/**
3
 * This file is part of the LdapToolsBundle package.
4
 *
5
 * (c) Chad Sikorra <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LdapTools\Bundle\LdapToolsBundle\Security\User;
12
13
use LdapTools\Exception\EmptyResultException;
14
use LdapTools\Exception\MultiResultException;
15
use LdapTools\Object\LdapObject;
16
use LdapTools\Object\LdapObjectCollection;
17
use LdapTools\Object\LdapObjectType;
18
use LdapTools\Utilities\LdapUtilities;
19
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
20
use Symfony\Component\Security\Core\User\UserInterface;
21
use Symfony\Component\Security\Core\User\UserProviderInterface;
22
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
23
use LdapTools\LdapManager;
24
25
/**
26
 * Loads a user from LDAP.
27
 *
28
 * @author Chad Sikorra <[email protected]>
29
 */
30
class LdapUserProvider implements UserProviderInterface
31
{
32
    /**
33
     * The base LdapUser class instantiated by this user provider.
34
     */
35
    const BASE_USER_CLASS = '\LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser';
36
37
    /**
38
     * @var LdapManager
39
     */
40
    protected $ldap;
41
42
    /**
43
     * @var array The map for the LDAP attribute names.
44
     */
45
    protected $attrMap = [];
46
47
    /**
48
     * @var array The role to LDAP group name map.
49
     */
50
    protected $roleMap = [];
51
52
    /**
53
     * @var array Map names to their LDAP attribute names when querying for LDAP groups used for roles.
54
     */
55
    protected $roleAttrMap = [
56
        'name' => 'name',
57
        'sid' => 'sid',
58
        'guid' => 'guid',
59
        'members' => 'members',
60
    ];
61
62
    /**
63
     * @var array Any additional LDAP attributes to select.
64
     */
65
    protected $attributes = [];
66
67
    /**
68
     * @var string
69
     */
70
    protected $userClass = self::BASE_USER_CLASS;
71
72
    /**
73
     * @var bool Whether or not to check group membership recursively when checking role membership.
74
     */
75
    protected $checkGroupsRecursively;
76
77
    /**
78
     * @var string|null The default role to be assigned to a user.
79
     */
80
    protected $defaultRole;
81
82
    /**
83
     * @var string The object type to search LDAP for.
84
     */
85
    protected $ldapObjectType = LdapObjectType::USER;
86
87
    /**
88
     * @var string The group object type when searching group membership.
89
     */
90
    protected $groupObjectType = LdapObjectType::GROUP;
91
    
92
    /**
93
     * @var string The container/OU to search for the user under.
94
     */
95
    protected $searchBase;
96
97
    /**
98
     * @var bool Whether or not user attributes should be re-queried on a refresh.
99
     */
100
    protected $refreshAttributes = true;
101
102
    /**
103
     * @var bool Whether or not user roles should be re-queried on a refresh.
104
     */
105
    protected $refreshRoles = true;
106
107
    /**
108
     * @param LdapManager $ldap
109
     * @param array $attrMap
110
     * @param array $roleMap
111
     * @param bool $checkGroupsRecursively
112
     */
113
    public function __construct(LdapManager $ldap, array $attrMap, array $roleMap, $checkGroupsRecursively = true)
114
    {
115
        $this->ldap = $ldap;
116
        $this->attrMap = $attrMap;
117
        $this->roleMap = $roleMap;
118
        $this->checkGroupsRecursively = $checkGroupsRecursively;
119
    }
120
121
    /**
122
     * Set the default role to add to a LDAP user.
123
     *
124
     * @param string|null $role
125
     */
126
    public function setDefaultRole($role)
127
    {
128
        if (is_string($role)) {
129
            $role = strtoupper($role);
130
        }
131
        $this->defaultRole = $role;
132
    }
133
134
    /**
135
     * Set the user class to be instantiated and returned from the LDAP provider.
136
     *
137
     * @param string $class
138
     */
139
    public function setUserClass($class)
140
    {
141
        if (!($class === self::BASE_USER_CLASS || is_subclass_of($class, self::BASE_USER_CLASS))) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if self::BASE_USER_CLASS can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
142
            throw new UnsupportedUserException(sprintf(
143
                'The LDAP user provider class "%s" must be an instance of "%s".',
144
                $class,
145
                self::BASE_USER_CLASS
146
            ));
147
        }
148
149
        $this->userClass = $class;
150
    }
151
152
    /**
153
     * Set any additional attributes to be selected for the LDAP user.
154
     *
155
     * @param array $attributes
156
     */
157
    public function setAttributes(array $attributes)
158
    {
159
        $this->attributes = $attributes;
160
    }
161
162
    /**
163
     * Set the LDAP object type that will be searched for.
164
     *
165
     * @param string $type
166
     */
167
    public function setLdapObjectType($type)
168
    {
169
        $this->ldapObjectType = $type;
170
    }
171
172
    /**
173
     * Set the LdapTools object type to search for group membership.
174
     * 
175
     * @param string $type
176
     */
177
    public function setRoleLdapType($type)
178
    {
179
        $this->groupObjectType = $type;
180
    }
181
182
    /**
183
     * Set the attribute name to LDAP name attributes used in querying LDAP groups for roles.
184
     * 
185
     * @param array $map
186
     */
187
    public function setRoleAttributeMap(array $map)
188
    {
189
        $this->roleAttrMap = $map;
190
    }
191
192
    /**
193
     * @param string $searchBase
194
     */
195
    public function setSearchBase($searchBase)
196
    {
197
        $this->searchBase = $searchBase;
198
    }
199
200
    /**
201
     * @param bool $refreshRoles
202
     */
203
    public function setRefreshRoles($refreshRoles)
204
    {
205
        $this->refreshRoles = $refreshRoles;
206
    }
207
208
    /**
209
     * @param bool $refreshAttributes
210
     */
211
    public function setRefreshAttributes($refreshAttributes)
212
    {
213
        $this->refreshAttributes = $refreshAttributes;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function loadUserByUsername($username)
220
    {
221
        return $this->setRolesForUser($this->searchForUser($this->attrMap['username'], $username));
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function refreshUser(UserInterface $user)
228
    {
229
        if (!$user instanceof LdapUser) {
230
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
231
        }
232
        if ($this->refreshAttributes) {
233
            $refreshedUser = $this->searchForUser($this->attrMap['guid'], $user->getLdapGuid());
234
        } else {
235
            $refreshedUser = $this->constructUserClass($user);
236
        }
237
        if ($this->refreshRoles) {
238
            $this->setRolesForUser($refreshedUser);
239
        } else {
240
            $refreshedUser->setRoles($user->getRoles());
241
        }
242
243
        return $refreshedUser;
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    public function supportsClass($class)
250
    {
251
        return ($class === self::BASE_USER_CLASS || is_subclass_of($class, self::BASE_USER_CLASS));
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if self::BASE_USER_CLASS can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
252
    }
253
254
    /**
255
     * Search for, and return, the LDAP user by a specific attribute.
256
     *
257
     * @param string $attribute
258
     * @param string $value
259
     * @return LdapUser
260
     */
261
    protected function searchForUser($attribute, $value)
262
    {
263
        try {
264
            $query = $this->ldap->buildLdapQuery()
265
                ->select($this->getAttributesToSelect())
266
                ->from($this->ldapObjectType)
267
                ->where([$attribute => $value]);
268
            if (!is_null($this->searchBase)) {
269
                $query->setBaseDn($this->searchBase);
270
            }
271
            $ldapUser = $query->getLdapQuery()->getSingleResult();
272
        } catch (EmptyResultException $e) {
273
            throw new UsernameNotFoundException(sprintf('Username "%s" was not found.', $value));
274
        } catch (MultiResultException $e) {
275
            throw new UsernameNotFoundException(sprintf('Multiple results for "%s" were found.', $value));
276
        }
277
278
        return $this->constructUserClass($ldapUser);
0 ignored issues
show
Bug introduced by
It seems like $ldapUser defined by $query->getLdapQuery()->getSingleResult() on line 271 can also be of type array; however, LdapTools\Bundle\LdapToo...r::constructUserClass() does only seem to accept object<LdapTools\Object\LdapObject>, 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...
279
    }
280
281
    /**
282
     * Get all the attributes that should be selected for when querying LDAP.
283
     *
284
     * @return array
285
     */
286
    protected function getAttributesToSelect()
287
    {
288
        $attributes = array_values($this->attrMap);
289
        if (!empty($this->attributes)) {
290
            $attributes = array_merge($attributes, $this->attributes);
291
        }
292
293
        return $attributes;
294
    }
295
296
    /**
297
     * Set the roles for the user based on group membership.
298
     *
299
     * @param LdapUser $user
300
     * @return LdapUser
301
     */
302
    protected function setRolesForUser(LdapUser $user)
303
    {
304
        if ($this->defaultRole) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->defaultRole of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
305
            $user->addRole($this->defaultRole);
306
        }
307
        $groups = $this->getGroupsForUser($user);
308
309
        foreach ($this->roleMap as $role => $roleGroups) {
310
            if ($this->hasGroupForRoles($roleGroups, $groups)) {
311
                $user->addRole($role);
312
            }
313
        }
314
315
        return $user;
316
    }
317
318
    /**
319
     * Check all of the groups that are valid for a specific role against all of the LDAP groups that the user belongs
320
     * to.
321
     * 
322
     * @param array $roleGroups
323
     * @param LdapObjectCollection $ldapGroups
324
     * @return bool
325
     */
326
    protected function hasGroupForRoles(array $roleGroups, LdapObjectCollection $ldapGroups)
327
    {
328
        foreach ($roleGroups as $roleGroup) {
329
            if (LdapUtilities::isValidLdapObjectDn($roleGroup)) {
330
                $attribute = 'dn';
331
            } elseif (preg_match(LdapUtilities::MATCH_GUID, $roleGroup)) {
332
                $attribute = $this->roleAttrMap['guid'];
333
            } elseif (preg_match(LdapUtilities::MATCH_SID, $roleGroup)) {
334
                $attribute = $this->roleAttrMap['sid'];
335
            } else {
336
                $attribute = $this->roleAttrMap['name'];
337
            }
338
339
            if ($this->hasGroupWithAttributeValue($ldapGroups, $attribute, $roleGroup)) {
340
                return true;
341
            }
342
        }
343
        
344
        return false;
345
    }
346
347
    /**
348
     * Check each LDAP group to see if any of them have an attribute with a specific value.
349
     * 
350
     * @param LdapObjectCollection $groups
351
     * @param string $attribute
352
     * @param string $value
353
     * @return bool
354
     */
355
    protected function hasGroupWithAttributeValue(LdapObjectCollection $groups, $attribute, $value)
356
    {
357
        $value = strtolower($value);
358
359
        /** @var LdapObject $group */
360
        foreach ($groups as $group) {
361
            if ($group->has($attribute) && strtolower($group->get($attribute)) == $value) {
362
                return true;
363
            }
364
        }
365
366
        return false;
367
    }
368
369
    /**
370
     * @param LdapUser $user
371
     * @return LdapObjectCollection
372
     */
373
    protected function getGroupsForUser(LdapUser $user)
374
    {
375
        $select = $this->roleAttrMap;
376
        unset($select['members']);
377
378
        $query = $this->ldap->buildLdapQuery()
379
            ->from($this->groupObjectType)
380
            ->select(array_values($select));
381
        
382
        if ($this->checkGroupsRecursively) {
383
            $query->where($query->filter()->hasMemberRecursively($user->getLdapGuid(), $this->roleAttrMap['members']));
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class LdapTools\Query\Builder\FilterBuilder as the method hasMemberRecursively() does only exist in the following sub-classes of LdapTools\Query\Builder\FilterBuilder: LdapTools\Query\Builder\ADFilterBuilder. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
384
        } else {
385
            $query->where([$this->roleAttrMap['members'] => $user->getLdapGuid()]);
386
        }
387
        
388
        return $query->getLdapQuery()->getResult();
389
    }
390
391
    /**
392
     * @param LdapObject $ldapObject
393
     * @return LdapUser
394
     */
395
    protected function constructUserClass(LdapObject $ldapObject)
396
    {
397
        $errorMessage = 'Unable to instantiate user class "%s". Error was: %s';
398
399
        try {
400
            $user = new $this->userClass($ldapObject, $this->attrMap);
401
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
402
            throw new UnsupportedUserException(sprintf($errorMessage, $this->userClass, $e->getMessage()));
403
        // Unlikely to help much in PHP 5.6, but oh well...
404
        } catch (\Exception $e) {
405
            throw new UnsupportedUserException(sprintf($errorMessage, $this->userClass, $e->getMessage()));
406
        }
407
408
        return $user;
409
    }
410
}
411