Completed
Push — master ( b792a7...d62054 )
by Chad
01:38
created

LdapRoleMapper::setRoles()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 11
nc 4
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\Connection\LdapConnection;
14
use LdapTools\LdapManager;
15
use LdapTools\Object\LdapObjectCollection;
16
use LdapTools\Utilities\LdapUtilities;
17
18
/**
19
 * Maps LDAP groups to Symfony Roles for a LdapUserInterface user.
20
 *
21
 * @author Chad Sikorra <[email protected]>
22
 */
23
class LdapRoleMapper
24
{
25
    /**
26
     * @var LdapManager
27
     */
28
    protected $ldap;
29
30
    /**
31
     * @var array
32
     */
33
    protected $options = [
34
        'check_groups_recursively' => true,
35
        'default_role' => 'ROLE_USER',
36
        'roles' => [],
37
        'role_ldap_type' => 'group',
38
        'role_attributes' => [
39
            'guid' => 'guid',
40
            'sid' => 'sid',
41
            'name' => 'name',
42
            'members' => 'members',
43
        ],
44
    ];
45
46
    /**
47
     * @param LdapManager $ldap
48
     * @param array $options
49
     */
50
    public function __construct(LdapManager $ldap, array $options)
51
    {
52
        $this->ldap = $ldap;
53
        $this->options = array_merge($this->options, $options);
54
    }
55
56
    /**
57
     * Set the roles for the user based on LDAP group membership.
58
     *
59
     * @param LdapUserInterface $user
60
     * @return LdapUserInterface
61
     */
62
    public function setRoles(LdapUserInterface $user)
63
    {
64
        $roles = [];
65
66
        if ($this->options['default_role']) {
67
            $roles[] = $this->options['default_role'];
68
        }
69
        
70
        if (!empty($this->options['roles'])) {
71
            $groups = $this->getGroupsForUser($user);
72
73
            foreach ($this->options['roles'] as $role => $roleGroups) {
74
                if ($this->hasGroupForRoles($roleGroups, $groups)) {
75
                    $roles[] = $role;
76
                }
77
            }
78
        }
79
80
        $user->setRoles($roles);
81
82
        return $user;
83
    }
84
85
    /**
86
     * Check all of the groups that are valid for a specific role against all of the LDAP groups that the user belongs
87
     * to.
88
     *
89
     * @param array $roleGroups
90
     * @param LdapObjectCollection $ldapGroups
91
     * @return bool
92
     */
93
    protected function hasGroupForRoles(array $roleGroups, LdapObjectCollection $ldapGroups)
94
    {
95
        foreach ($roleGroups as $roleGroup) {
96
            if (LdapUtilities::isValidLdapObjectDn($roleGroup)) {
97
                $attribute = 'dn';
98
            } elseif (preg_match(LdapUtilities::MATCH_GUID, $roleGroup)) {
99
                $attribute = $this->options['role_attributes']['guid'];
100
            } elseif (preg_match(LdapUtilities::MATCH_SID, $roleGroup)) {
101
                $attribute = $this->options['role_attributes']['sid'];
102
            } else {
103
                $attribute = $this->options['role_attributes']['name'];
104
            }
105
106
            if ($this->hasGroupWithAttributeValue($ldapGroups, $attribute, $roleGroup)) {
107
                return true;
108
            }
109
        }
110
111
        return false;
112
    }
113
114
    /**
115
     * Check each LDAP group to see if any of them have an attribute with a specific value.
116
     *
117
     * @param LdapObjectCollection $groups
118
     * @param string $attribute
119
     * @param string $value
120
     * @return bool
121
     */
122
    protected function hasGroupWithAttributeValue(LdapObjectCollection $groups, $attribute, $value)
123
    {
124
        $value = strtolower($value);
125
126
        /** @var \LdapTools\Object\LdapObject $group */
127
        foreach ($groups as $group) {
128
            if ($group->has($attribute) && strtolower($group->get($attribute)) === $value) {
129
                return true;
130
            }
131
        }
132
133
        return false;
134
    }
135
136
    /**
137
     * @param LdapUserInterface $user
138
     * @return LdapObjectCollection
139
     */
140
    protected function getGroupsForUser(LdapUserInterface $user)
141
    {
142
        $select = $this->options['role_attributes'];
143
        unset($select['members']);
144
145
        $query = $this->ldap->buildLdapQuery()
146
            ->from($this->options['role_ldap_type'])
147
            ->select(array_values($select));
148
149
        /**
150
         * @todo How to support recursive group checks for all LDAP types? Need a recursive method check of sorts...
151
         */
152
        if ($this->ldap->getConnection()->getConfig()->getLdapType() === LdapConnection::TYPE_AD && $this->options['check_groups_recursively']) {
153
            $query->where($query->filter()->hasMemberRecursively($user->getLdapGuid(), $this->options['role_attributes']['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...
154
        } else {
155
            $query->where([$this->options['role_attributes']['members'] => $user->getLdapGuid()]);
156
        }
157
158
        return $query->getLdapQuery()->getResult();
159
    }
160
}
161