Passed
Push — master ( a6d2ac...35bc93 )
by Gabor
05:20
created

AclMiddleware::setIdentityForTemplate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 2
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 5.6
6
 *
7
 * @copyright 2012 - 2016 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
namespace WebHemi\Middleware\Security;
13
14
use Exception;
15
use WebHemi\Adapter\Auth\AuthAdapterInterface;
16
use WebHemi\Adapter\Http\ResponseInterface;
17
use WebHemi\Adapter\Http\ServerRequestInterface;
18
use WebHemi\Application\EnvironmentManager;
19
use WebHemi\Data\Coupler\UserGroupToPolicyCoupler;
20
use WebHemi\Data\Coupler\UserToGroupCoupler;
21
use WebHemi\Data\Coupler\UserToPolicyCoupler;
22
use WebHemi\Data\Entity\AccessManagement\PolicyEntity;
23
use WebHemi\Data\Entity\AccessManagement\ResourceEntity;
24
use WebHemi\Data\Entity\ApplicationEntity;
25
use WebHemi\Data\Entity\User\UserMetaEntity;
26
use WebHemi\Data\Entity\User\UserEntity;
27
use WebHemi\Data\Storage\AccessManagement\ResourceStorage;
28
use WebHemi\Data\Storage\ApplicationStorage;
29
use WebHemi\Data\Storage\User\UserMetaStorage;
30
use WebHemi\Middleware\Action;
31
use WebHemi\Middleware\MiddlewareInterface;
32
33
/**
34
 * Class AclMiddleware.
35
 */
36
class AclMiddleware implements MiddlewareInterface
37
{
38
    /** @var EnvironmentManager */
39
    private $environmentManager;
40
    /** @var AuthAdapterInterface */
41
    private $authAdapter;
42
    /** @var UserToPolicyCoupler */
43
    private $userToPolicyCoupler;
44
    /** @var UserToGroupCoupler */
45
    private $userToGroupCoupler;
46
    /** @var UserGroupToPolicyCoupler */
47
    private $userGroupToPolicyCoupler;
48
    /** @var ApplicationStorage */
49
    private $applicationStorage;
50
    /** @var ResourceStorage */
51
    private $resourceStorage;
52
    /** @var UserMetaStorage */
53
    private $userMetaStorage;
54
    /** @var array */
55
    private $middlewareWhiteList = [
56
        Action\Auth\LoginAction::class,
57
        Action\Auth\LogoutAction::class,
58
    ];
59
60
    /**
61
     * AclMiddleware constructor.
62
     * @param AuthAdapterInterface     $authAdapter
63
     * @param EnvironmentManager       $environmentManager
64
     * @param UserToPolicyCoupler      $userToPolicyCoupler
65
     * @param UserToGroupCoupler       $userToGroupCoupler
66
     * @param UserGroupToPolicyCoupler $userGroupToPolicyCoupler
67
     * @param ApplicationStorage       $applicationStorage
68
     * @param ResourceStorage          $resourceStorage
69
     * @param UserMetaStorage          $userMetaStorage
70
     */
71
    public function __construct(
72
        AuthAdapterInterface $authAdapter,
73
        EnvironmentManager $environmentManager,
74
        UserToPolicyCoupler $userToPolicyCoupler,
75
        UserToGroupCoupler $userToGroupCoupler,
76
        UserGroupToPolicyCoupler $userGroupToPolicyCoupler,
77
        ApplicationStorage $applicationStorage,
78
        ResourceStorage $resourceStorage,
79
        UserMetaStorage $userMetaStorage
80
    ) {
81
        $this->authAdapter = $authAdapter;
82
        $this->environmentManager = $environmentManager;
83
        $this->userToPolicyCoupler = $userToPolicyCoupler;
84
        $this->userToGroupCoupler = $userToGroupCoupler;
85
        $this->userGroupToPolicyCoupler = $userGroupToPolicyCoupler;
86
        $this->applicationStorage = $applicationStorage;
87
        $this->resourceStorage = $resourceStorage;
88
        $this->userMetaStorage = $userMetaStorage;
89
    }
90
91
    /**
92
     * A middleware is a callable. It can do whatever is appropriate with the Request and Response objects.
93
     * The only hard requirement is that a middleware MUST return an instance of \Psr\Http\Message\ResponseInterface.
94
     * Each middleware SHOULD invoke the next middleware and pass it Request and Response objects as arguments.
95
     *
96
     * @param ServerRequestInterface $request
97
     * @param ResponseInterface      $response
98
     * @throws Exception
99
     * @return ResponseInterface
100
     */
101
    public function __invoke(ServerRequestInterface&$request, ResponseInterface $response)
102
    {
103
        $actionMiddleware = $request->getAttribute(ServerRequestInterface::REQUEST_ATTR_RESOLVED_ACTION_CLASS);
104
        $identity = false;
105
106
        if (in_array($actionMiddleware, $this->middlewareWhiteList)) {
107
            return $response;
108
        }
109
110
        if ($this->authAdapter->hasIdentity()) {
111
            $identity = $this->authAdapter->getIdentity();
112
        }
113
114
        if ($identity instanceof UserEntity) {
115
            $selectedApplication = $this->environmentManager->getSelectedApplication();
116
            /** @var ApplicationEntity $applicationEntity */
117
            $applicationEntity = $this->applicationStorage->getApplicationByName($selectedApplication);
118
            /** @var ResourceEntity $resourceEntity */
119
            $resourceEntity = $this->resourceStorage->getResourceByName($actionMiddleware);
120
            // First we check the group policies
121
            /** @var array<UserGroupEntity> $userGroups */
122
            $userGroups = $this->userToGroupCoupler->getEntityDependencies($identity);
123
            /** @var array<PolicyEntity> $userGroupPolicies */
124
            $userGroupPolicies = [];
125
            foreach ($userGroups as $userGroupEntity) {
126
                /** @var array<PolicyEntity> $groupPolicies */
127
                $groupPolicies = $this->userGroupToPolicyCoupler->getEntityDependencies($userGroupEntity);
128
                $userGroupPolicies = array_merge($userGroupPolicies, $groupPolicies);
129
            }
130
            $hasAccess = $this->checkPolicies($userGroupPolicies, $applicationEntity, $resourceEntity);
131
132
            // Then we check the personal policies
133
            /** @var array<PolicyEntity> $userPolicies */
134
            $userPolicies = $this->userToPolicyCoupler->getEntityDependencies($identity);
135
            $hasAccess = $hasAccess && $this->checkPolicies($userPolicies, $applicationEntity, $resourceEntity);
136
137
            $request = $this->setIdentityForTemplate($request, $identity);
138
139
            if (!$hasAccess) {
140
                $response = $response->withStatus(ResponseInterface::STATUS_FORBIDDEN, 'Forbidden');
141
            }
142
        } else {
143
            $appUri = rtrim($this->environmentManager->getSelectedApplicationUri(), '/');
144
            $response = $response->withStatus(ResponseInterface::STATUS_REDIRECT, 'Found')
145
                ->withHeader('Location', $appUri.'/auth/login');
146
        }
147
148
        return $response;
149
    }
150
151
    /**
152
     * Checks policies for application and resource
153
     *
154
     * @param array<PolicyEntity>      $policies
155
     * @param ApplicationEntity|null   $applicationEntity
156
     * @param ResourceEntity|null      $resourceEntity
157
     * @return bool
158
     */
159
    private function checkPolicies(
160
        array $policies,
161
        ApplicationEntity $applicationEntity = null,
162
        ResourceEntity $resourceEntity = null
163
    ) {
164
        // We assume the best case: the user has access
165
        $hasAccess = true;
166
167
        /** @var PolicyEntity $policyEntity */
168
        foreach ($policies as $policyEntity) {
169
            $hasAccess = $hasAccess && $this->checkPolicy($policyEntity, $applicationEntity, $resourceEntity);
170
        }
171
172
        return $hasAccess;
173
    }
174
175
    /**
176
     * @param PolicyEntity           $policyEntity
177
     * @param ApplicationEntity|null $applicationEntity
178
     * @param ResourceEntity|null    $resourceEntity
179
     * @return bool
180
     */
181
    private function checkPolicy(
182
        PolicyEntity $policyEntity,
183
        ApplicationEntity $applicationEntity = null,
184
        ResourceEntity $resourceEntity = null
185
    ) {
186
        $applicationId = $applicationEntity ? $applicationEntity->getApplicationId() : null;
187
        $resourceId = $resourceEntity ? $resourceEntity->getResourceId() : null;
188
        $policyApplicationId = $policyEntity->getApplicationId();
189
        $policyResourceId = $policyEntity->getResourceId();
190
191
        // The user has access when:
192
        // - user has a policy that connected to the current application OR any application AND
193
        // - user has a policy that connected to the current resource OR any resource
194
        if (($policyApplicationId == null || $policyApplicationId == $applicationId)
195
            && ($policyResourceId == null || $policyResourceId == $resourceId)
196
        ) {
197
            return $policyEntity->getAllowed();
198
        }
199
200
        // At this point we know that the current policy doesn't belong to this application or resource, so no need
201
        // to block the user.
202
        return true;
203
    }
204
205
    /**
206
     * Set identified user data for the templates
207
     *
208
     * @param ServerRequestInterface $request
209
     * @param UserEntity             $identity
210
     * @return ServerRequestInterface
211
     */
212
    private function setIdentityForTemplate(ServerRequestInterface $request, UserEntity $identity)
213
    {
214
        // Set authenticated user for the templates
215
        $templateData = $request->getAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA, []);
216
        $templateData['authenticated_user'] = $identity;
217
        $templateData['authenticated_user_meta'] = [];
218
        $userMeta = $this->userMetaStorage->getUserMetaForUserId($identity->getUserId());
219
        /** @var UserMetaEntity $metaEntity */
220
        foreach ($userMeta as $metaEntity) {
0 ignored issues
show
Bug introduced by
The expression $userMeta of type null|array<integer,objec...y\DataEntityInterface>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
221
            $templateData['authenticated_user_meta'][$metaEntity->getMetaKey()] = $metaEntity->getMetaData();
222
        }
223
224
        return $request->withAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA, $templateData);
225
    }
226
}
227