Completed
Push — master ( 06c1ce...67d37c )
by Jeroen
06:20
created

AdminBundle/Helper/Security/Acl/AclHelper.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Kunstmaan\AdminBundle\Helper\Security\Acl;
4
5
use Doctrine\ORM\EntityManager;
6
use Doctrine\ORM\Mapping\QuoteStrategy;
7
use Doctrine\ORM\Query;
8
use Doctrine\ORM\Query\Parameter;
9
use Doctrine\ORM\Query\ResultSetMapping;
10
use Doctrine\ORM\QueryBuilder;
11
use InvalidArgumentException;
12
use Kunstmaan\AdminBundle\Helper\Security\Acl\Permission\MaskBuilder;
13
use Kunstmaan\AdminBundle\Helper\Security\Acl\Permission\PermissionDefinition;
14
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
15
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
16
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
17
18
/**
19
 * AclHelper is a helper class to help setting the permissions when querying using ORM
20
 *
21
 * @see https://gist.github.com/1363377
22
 */
23
class AclHelper
24
{
25
    /**
26
     * @var EntityManager
27
     */
28
    private $em = null;
29
30
    /**
31
     * @var TokenStorageInterface
32
     */
33
    private $tokenStorage = null;
34
35
    /**
36
     * @var QuoteStrategy
37
     */
38
    private $quoteStrategy = null;
39
40
    /**
41
     * @var RoleHierarchyInterface
42
     */
43
    private $roleHierarchy = null;
44
45
    /**
46
     * @var bool
47
     */
48
    private $permissionsEnabled;
49
50
    /**
51
     * Constructor.
52
     *
53
     * @param EntityManager          $em           The entity manager
54
     * @param TokenStorageInterface  $tokenStorage The security token storage
55
     * @param RoleHierarchyInterface $rh           The role hierarchies
56
     */
57 5 View Code Duplication
    public function __construct(EntityManager $em, TokenStorageInterface $tokenStorage, RoleHierarchyInterface $rh, $permissionsEnabled = true)
58
    {
59 5
        $this->em = $em;
60 5
        $this->tokenStorage = $tokenStorage;
61 5
        $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
62 5
        $this->roleHierarchy = $rh;
63 5
        $this->permissionsEnabled = $permissionsEnabled;
64 5
    }
65
66
    /**
67
     * Clone specified query with parameters.
68
     *
69
     * @param Query $query
70
     *
71
     * @return Query
72
     */
73 2
    protected function cloneQuery(Query $query)
74
    {
75 2
        $aclAppliedQuery = clone $query;
76 2
        $params = $query->getParameters();
77
        /* @var $param Parameter */
78 2
        foreach ($params as $param) {
79 2
            $aclAppliedQuery->setParameter($param->getName(), $param->getValue(), $param->getType());
80
        }
81
82 2
        return $aclAppliedQuery;
83
    }
84
85
    /**
86
     * Apply the ACL constraints to the specified query builder, using the permission definition
87
     *
88
     * @param QueryBuilder         $queryBuilder  The query builder
89
     * @param PermissionDefinition $permissionDef The permission definition
90
     *
91
     * @return Query
92
     */
93 2
    public function apply(QueryBuilder $queryBuilder, PermissionDefinition $permissionDef)
94
    {
95 2
        if (!$this->permissionsEnabled) {
96
            return $queryBuilder->getQuery();
97
        }
98
99 2
        $whereQueryParts = $queryBuilder->getDQLPart('where');
100 2
        if (empty($whereQueryParts)) {
101 2
            $queryBuilder->where('1 = 1'); // this will help in cases where no where query is specified
102
        }
103
104 2
        $query = $this->cloneQuery($queryBuilder->getQuery());
105
106 2
        $builder = new MaskBuilder();
107 2 View Code Duplication
        foreach ($permissionDef->getPermissions() as $permission) {
108 2
            $mask = \constant(\get_class($builder) . '::MASK_' . strtoupper($permission));
109 2
            $builder->add($mask);
110
        }
111 2
        $query->setHint('acl.mask', $builder->get());
112 2
        $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Kunstmaan\AdminBundle\Helper\Security\Acl\AclWalker');
113
114 2
        $rootEntity = $permissionDef->getEntity();
115 2
        $rootAlias = $permissionDef->getAlias();
116
        // If either alias or entity was not specified - use default from QueryBuilder
117 2
        if (empty($rootEntity) || empty($rootAlias)) {
118 2
            $rootEntities = $queryBuilder->getRootEntities();
119 2
            $rootAliases = $queryBuilder->getRootAliases();
120 2
            $rootEntity = $rootEntities[0];
121 2
            $rootAlias = $rootAliases[0];
122
        }
123 2
        $query->setHint('acl.root.entity', $rootEntity);
124 2
        $query->setHint('acl.extra.query', $this->getPermittedAclIdsSQLForUser($query));
125
126 2
        $classMeta = $this->em->getClassMetadata($rootEntity);
127 2
        $entityRootTableName = $this->quoteStrategy->getTableName(
128 2
            $classMeta,
129 2
            $this->em->getConnection()->getDatabasePlatform()
130
        );
131 2
        $query->setHint('acl.entityRootTableName', $entityRootTableName);
132 2
        $query->setHint('acl.entityRootTableDqlAlias', $rootAlias);
133
134 2
        return $query;
135
    }
136
137
    /**
138
     * This query works well with small offset, but if want to use it with large offsets please refer to the link on how to implement
139
     * http://www.scribd.com/doc/14683263/Efficient-Pagination-Using-MySQL
140
     * This will only check permissions on the first entity added in the from clause, it will not check permissions
141
     * By default the number of rows returned are 10 starting from 0
142
     *
143
     * @param Query $query
144
     *
145
     * @return string
146
     */
147 3
    private function getPermittedAclIdsSQLForUser(Query $query)
148
    {
149 3
        $aclConnection = $this->em->getConnection();
150 3
        $databasePrefix = is_file($aclConnection->getDatabase()) ? '' : $aclConnection->getDatabase().'.';
151 3
        $mask = $query->getHint('acl.mask');
152 3
        $rootEntity = '"' . str_replace('\\', '\\\\', $query->getHint('acl.root.entity')) . '"';
153
154
        /* @var $token TokenInterface */
155 3
        $token = $this->tokenStorage->getToken();
156 3
        $userRoles = array();
157 3
        $user = null;
158 3 View Code Duplication
        if (!\is_null($token)) {
159 3
            $user = $token->getUser();
160 3
            if (method_exists($this->roleHierarchy, 'getReachableRoleNames')) {
161 3
                $userRoles = $this->roleHierarchy->getReachableRoleNames($token->getRoleNames());
162
            } else {
163
                // Symfony 3.4 compatibility
164
                $userRoles = $this->roleHierarchy->getReachableRoles($token->getRoles());
165
            }
166
        }
167
168
        // Security context does not provide anonymous role automatically.
169 3
        $uR = array('"IS_AUTHENTICATED_ANONYMOUSLY"');
170
171 3 View Code Duplication
        foreach ($userRoles as $role) {
172
            // The reason we ignore this is because by default FOSUserBundle adds ROLE_USER for every user
173 3
            if (is_string($role)) {
174 3
                if ($role !== 'ROLE_USER') {
175 3
                    $uR[] = '"' . $role . '"';
176
                }
177
            } else {
178
                // Symfony 3.4 compatibility
179
                if ($role->getRole() !== 'ROLE_USER') {
180
                    $uR[] = '"' . $role->getRole() . '"';
181
                }
182
            }
183
        }
184 3
        $uR = array_unique($uR);
185 3
        $inString = implode(' OR s.identifier = ', $uR);
186
187 3 View Code Duplication
        if (\is_object($user)) {
188 2
            $inString .= ' OR s.identifier = "' . str_replace(
189 2
                '\\',
190 2
                '\\\\',
191 2
                \get_class($user)
192 2
            ) . '-' . $user->getUserName() . '"';
0 ignored issues
show
The method getUserName does only exist in Symfony\Component\Security\Core\User\UserInterface, but not in Stringable.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
193
        }
194
195
        $selectQuery = <<<SELECTQUERY
196 3
SELECT DISTINCT o.object_identifier as id FROM {$databasePrefix}acl_object_identities as o
197 3
INNER JOIN {$databasePrefix}acl_classes c ON c.id = o.class_id
198 3
LEFT JOIN {$databasePrefix}acl_entries e ON (
199
    e.class_id = o.class_id AND (e.object_identity_id = o.id
200 3
    OR {$aclConnection->getDatabasePlatform()->getIsNullExpression('e.object_identity_id')})
201
)
202 3
LEFT JOIN {$databasePrefix}acl_security_identities s ON (
203
s.id = e.security_identity_id
204
)
205 3
WHERE c.class_type = {$rootEntity}
206 3
AND (s.identifier = {$inString})
207 3
AND e.mask & {$mask} > 0
208
SELECTQUERY;
209
210 3
        return $selectQuery;
211
    }
212
213
    /**
214
     * Returns valid IDs for a specific entity with ACL restrictions for current user applied
215
     *
216
     * @param PermissionDefinition $permissionDef
217
     *
218
     * @throws InvalidArgumentException
219
     *
220
     * @return array
221
     */
222 2
    public function getAllowedEntityIds(PermissionDefinition $permissionDef)
223
    {
224 2
        $rootEntity = $permissionDef->getEntity();
225 2
        if (empty($rootEntity)) {
226 1
            throw new InvalidArgumentException('You have to provide an entity class name!');
227
        }
228 1
        $builder = new MaskBuilder();
229 1 View Code Duplication
        foreach ($permissionDef->getPermissions() as $permission) {
230 1
            $mask = \constant(\get_class($builder) . '::MASK_' . strtoupper($permission));
231 1
            $builder->add($mask);
232
        }
233
234 1
        $query = new Query($this->em);
235 1
        $query->setHint('acl.mask', $builder->get());
236 1
        $query->setHint('acl.root.entity', $rootEntity);
237 1
        $sql = $this->getPermittedAclIdsSQLForUser($query);
238
239 1
        $rsm = new ResultSetMapping();
240 1
        $rsm->addScalarResult('id', 'id');
241 1
        $nativeQuery = $this->em->createNativeQuery($sql, $rsm);
242
243
        $transform = function ($item) {
244 1
            return $item['id'];
245 1
        };
246 1
        $result = array_map($transform, $nativeQuery->getScalarResult());
247
248 1
        return $result;
249
    }
250
251
    /**
252
     * @return null|TokenStorageInterface
253
     */
254 1
    public function getTokenStorage()
255
    {
256 1
        return $this->tokenStorage;
257
    }
258
}
259