Passed
Push — main ( 1fbbbe...fd4d73 )
by Michiel
16:27 queued 12:04
created

CommandAuthorizationService::logDenyRA()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 11
rs 10
1
<?php
2
/**
3
 * Copyright 2010 SURFnet B.V.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *     http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
0 ignored issues
show
Coding Style introduced by
Missing @link tag in file comment
Loading history...
17
18
namespace Surfnet\StepupMiddleware\ApiBundle\Authorization\Service;
19
20
use Psr\Log\LoggerInterface;
21
use Surfnet\Stepup\Identity\Value\IdentityId;
22
use Surfnet\Stepup\Identity\Value\Institution;
23
use Surfnet\Stepup\Identity\Value\RegistrationAuthorityRole;
24
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\IdentityService;
25
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\WhitelistService;
26
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\Command;
27
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\RaExecutable;
28
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\SelfAsserted;
29
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\SelfServiceExecutable;
30
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\CreateIdentityCommand;
31
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\ExpressLocalePreferenceCommand;
32
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\RevokeOwnRecoveryTokenCommand;
33
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\RevokeRegistrantsRecoveryTokenCommand;
34
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\RevokeRegistrantsSecondFactorCommand;
35
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\UpdateIdentityCommand;
36
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\VetSecondFactorCommand;
37
38
/**
39
 * Verify if a given command may be executed.
40
 *
41
 * The service can be used to test if an institution is on the allow list. If not, the command may not be executed.
42
 *
43
 * Three roles are known to the CommandAuthorizationService:
44
 * 1. SRAA (super registration authority administrator). The admin user to rule them all.
45
 * 2. RAA (registration authority administrator). Allowed to perform additional administrative commands like selecting
46
 *    new RA(A)s from a list of candidates.
47
 * 3. RA (registration authority) the most basic administrative role. Allows for token vetting and revocation.
48
 *
49
 * Next, the  maySelfServiceCommandBeExecutedOnBehalfOf and mayRACommandBeExecutedOnBehalfOf methods test if a
50
 * RA or SS command may be processed by the identity that invoked the command. Some rules are applied here.
51
 *
52
 * 1. A SRAA user may always execute the command
53
 * 2. Certain commands are actionable with a RA role. When the identity is RAA, the identity is also allowed to run
54
 *    the command.
55
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
0 ignored issues
show
Coding Style introduced by
There must be exactly one blank line before the tags in a doc comment
Loading history...
56
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
57
class CommandAuthorizationService
58
{
59
    /**
60
     * @var WhitelistService
61
     */
62
    private $whitelistService;
63
    /**
64
     * @var IdentityService
65
     */
66
    private $identityService;
67
    /**
68
     * @var AuthorizationContextService
69
     */
70
    private $authorizationContextService;
71
    /**
72
     * @var LoggerInterface
73
     */
74
    private $logger;
75
76
    public function __construct(
77
        WhitelistService $whitelistService,
78
        IdentityService $identityService,
79
        LoggerInterface $logger,
80
        AuthorizationContextService $authorizationContextService
81
    ) {
82
        $this->logger = $logger;
83
        $this->authorizationContextService = $authorizationContextService;
84
        $this->whitelistService = $whitelistService;
85
        $this->identityService = $identityService;
86
    }
87
88
    /**
89
     * @param Institution $institution
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
90
     * @param IdentityId|null $actorId
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
91
     * @return bool
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
92
     */
93
    public function isInstitutionWhitelisted(Institution $institution, IdentityId $actorId = null)
94
    {
95
        // If the actor is SRAA all actions should be allowed
96
        if (!is_null($actorId) && $this->isSraa($actorId)) {
97
            return true;
98
        }
99
100
        if ($this->whitelistService->isWhitelisted($institution->getInstitution())) {
101
            return true;
102
        }
103
104
        return false;
105
    }
106
107
    public function maySelfServiceCommandBeExecutedOnBehalfOf(Command $command, IdentityId $actorId = null): bool
108
    {
109
        $commandName = get_class($command);
110
        $identityId = $actorId ? $actorId->getIdentityId() : null;
111
112
        // Assert Self Service command could be executed
113
        if ($command instanceof SelfServiceExecutable) {
114
            $this->logger->notice('Asserting a SelfService command');
115
116
            // If the actor is SRAA all actions should be allowed
117
            if ($this->isSraa($actorId)) {
118
                $this->logAllowSelfService(
119
                    'SRAA user is always allowed to record SelfService commands',
120
                    $commandName,
121
                    $identityId
122
                );
123
                return true;
124
            }
125
126
            // Self Asserted token registration is allowed for SelfServiceExecutable commands
127
            if ($command instanceof SelfAsserted) {
128
                $this->logAllowSelfService(
129
                    'Allowing execution of a SelfAsserted command',
130
                    $commandName,
131
                    $identityId
132
                );
133
                return true;
134
            }
135
136
            // the CreateIdentityCommand is used to create an Identity for a new user,
137
            // the UpdateIdentityCommand is used to update name or email of an identity
138
            // Both are only sent by the SS when the Identity is not logged in yet,
139
            // thus there is no Metadata::actorInstitution,
140
            if ($command instanceof CreateIdentityCommand || $command instanceof UpdateIdentityCommand) {
141
                $this->logAllowSelfService(
142
                    'Allowing execution of a CreateIdentityCommand or UpdateIdentityCommand command',
143
                    $commandName,
144
                    $identityId
145
                );
146
                return true;
147
            }
148
149
            // Validate if the actor is the user
150
            if ($command->getIdentityId() !== $actorId->getIdentityId()) {
0 ignored issues
show
Bug introduced by
The method getIdentityId() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

150
            if ($command->getIdentityId() !== $actorId->/** @scrutinizer ignore-call */ getIdentityId()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
151
                $this->logDenySelfService(
152
                    'The actor identity id does not match that of the identity id that was recorded in the command',
153
                    $commandName,
154
                    $identityId
155
                );
156
                return false;
157
            }
158
        }
159
        return true;
160
    }
161
162
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $command should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $actorId should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $actorInstitution should have a doc-comment as per coding-style.
Loading history...
163
     * @SuppressWarnings(PHPMD.CyclomaticComplexity) - To keep the method readable, increased CC is allowed
0 ignored issues
show
Coding Style introduced by
Tag value for @SuppressWarnings(PHPMD.CyclomaticComplexity) tag indented incorrectly; expected 2 spaces but found 1
Loading history...
164
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
165
     * @SuppressWarnings(PHPMD.NPathComplexity)
166
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
167
    public function mayRaCommandBeExecutedOnBehalfOf(
168
        Command $command,
169
        IdentityId $actorId = null,
170
        Institution $actorInstitution = null
171
    ): bool {
172
        $commandName = get_class($command);
173
        $identityId = $actorId ? $actorId->getIdentityId() : null;
174
175
        $this->logger->notice('Running the mayRaCommandBeExecutedOnBehalfOf sequence');
176
        // Assert RA(A) specific authorizations
177
        if ($command instanceof RaExecutable) {
178
            $this->logger->notice('Asserting a RA command');
179
180
            // No additional FGA authorization is required for this shared (SS/RA) command
181
            if ($command instanceof ExpressLocalePreferenceCommand) {
182
                $this->logAllowRa(
183
                    'RA(A) is always allowed to perform the ExpressLocalePreferenceCommand',
184
                    $commandName,
185
                    $identityId
186
                );
187
                return true;
188
            }
189
190
            // The actor metadata should be set
191
            if (is_null($actorId) || is_null($actorInstitution)) {
192
                $this->logDenyRA(
193
                    'ActorId and/or actorInstitution is missing in mayRaCommandBeExecutedOnBehalfOf',
194
                    $commandName,
195
                    $identityId
196
                );
197
                return false;
198
            }
199
200
            // If the actor is SRAA all actions are allowed
201
            if ($this->isSraa($actorId)) {
202
                $this->logAllowRa(
203
                    'SRAA is always allowed to execute RA commands',
204
                    $commandName,
205
                    $identityId
206
                );
207
                return true;
208
            }
209
210
            $raInstitution = $command->getRaInstitution();
211
            if (is_null($raInstitution)) {
212
                $raInstitution = $actorInstitution->getInstitution();
213
            }
214
215
            $this->logger->notice(sprintf('RA institution = %s', $raInstitution));
216
217
            $roleRequirement = RegistrationAuthorityRole::raa();
218
219
            // the VetSecondFactorCommand is used to vet a second factor for a user
220
            // the RevokeRegistrantsSecondFactorCommand is used to revoke a user's secondfactor
221
            // the RevokeRegistrantsRecoveryTokenCommand is used to revoke a user's recovery token
222
            // All three are only sent by the RA where the minimal role requirement is RA
223
            // all the other actions require RAA rights
224
            if ($command instanceof VetSecondFactorCommand ||
225
                $command instanceof RevokeRegistrantsSecondFactorCommand ||
0 ignored issues
show
Coding Style introduced by
Each line in a multi-line IF statement must begin with a boolean operator
Loading history...
226
                $command instanceof RevokeRegistrantsRecoveryTokenCommand
0 ignored issues
show
Coding Style introduced by
Each line in a multi-line IF statement must begin with a boolean operator
Loading history...
227
            ) {
228
                $this->logger->notice('VetSecondFactorCommand and RevokeRegistrantsSecondFactorCommand require a RA role');
229
                $roleRequirement = RegistrationAuthorityRole::ra();
230
                // Use the institution of the identity (the user vetting or having his token revoked).
231
                $identity = $this->identityService->find($command->identityId);
232
                if (!$identity) {
233
                    $this->logDenyRA(
234
                        'Unable to find the identity of the user that is being vetted, or revoked',
235
                        $commandName,
236
                        $identityId
237
                    );
238
                    return false;
239
                }
240
                $this->logger->notice(
241
                    sprintf(
242
                        'Changed RA institution (before %s) to identity institution: %s',
243
                        $raInstitution,
244
                        $identity->institution->getInstitution()
245
                    )
246
                );
247
                $raInstitution = $identity->institution->getInstitution();
248
            }
249
250
            $authorizationContext = $this->authorizationContextService->buildInstitutionAuthorizationContext(
251
                $actorId,
252
                $roleRequirement
253
            );
254
255
            $this->logger->notice(
256
                sprintf(
257
                    'Identity is authorized RA(A) role in institutions: %s',
258
                    implode(',', $authorizationContext->getInstitutions()->serialize())
259
                )
260
            );
261
262
            if (!$authorizationContext->getInstitutions()->contains(new Institution($raInstitution))) {
263
                $this->logDenyRA(
264
                    sprintf(
265
                        'Identity is not RA(A) for the specified RA institution, "%s". Allowed institutions: "%s"',
266
                        $raInstitution,
267
                        implode(',', $authorizationContext->getInstitutions()->serialize())
268
                    ),
269
                    $commandName,
270
                    $identityId
271
                );
272
                return false;
273
            }
274
        }
275
        $this->logAllowRa(
276
            'Allowed',
277
            $commandName,
278
            $identityId
279
        );
280
        return true;
281
    }
282
283
    private function isSraa(IdentityId $actorId = null): bool
0 ignored issues
show
Coding Style introduced by
Private method name "CommandAuthorizationService::isSraa" must be prefixed with an underscore
Loading history...
284
    {
285
        if (is_null($actorId)) {
286
            return false;
287
        }
288
289
        $registrationAuthorityCredentials = $this->identityService->findRegistrationAuthorityCredentialsOf($actorId->getIdentityId());
290
        if (!$registrationAuthorityCredentials) {
291
            return false;
292
        }
293
294
        if (!$registrationAuthorityCredentials->isSraa()) {
295
            return false;
296
        }
297
        return true;
298
    }
299
300
    private function logAllowSelfService(string $message, string $commandName, ?string $identityId): void
0 ignored issues
show
Coding Style introduced by
Private method name "CommandAuthorizationService::logAllowSelfService" must be prefixed with an underscore
Loading history...
301
    {
302
        if (!$identityId) {
303
            $identityId = '"unknown identityId"';
304
        }
305
306
        $this->logger->notice(
307
            sprintf(
308
                'Allowing SelfService command %s for identity %s. With message "%s"',
309
                $commandName,
310
                $identityId,
311
                $message
312
            )
313
        );
314
    }
315
316
    private function logDenySelfService(string $message, string $commandName, ?string $identityId): void
0 ignored issues
show
Coding Style introduced by
Private method name "CommandAuthorizationService::logDenySelfService" must be prefixed with an underscore
Loading history...
317
    {
318
        if (!$identityId) {
319
            $identityId = '"unknown identityId"';
320
        }
321
        $this->logger->notice(
322
            sprintf(
323
                'Denying SelfService command %s for identity %s. With message "%s"',
324
                $commandName,
325
                $identityId,
326
                $message
327
            )
328
        );
329
    }
330
331
    private function logAllowRa(string $message, string $commandName, ?string $identityId): void
0 ignored issues
show
Coding Style introduced by
Private method name "CommandAuthorizationService::logAllowRa" must be prefixed with an underscore
Loading history...
332
    {
333
        if (!$identityId) {
334
            $identityId = '"unknown identityId"';
335
        }
336
        $this->logger->notice(
337
            sprintf(
338
                'Allowing RA command %s for identity %s. With message "%s"',
339
                $commandName,
340
                $identityId,
341
                $message
342
            )
343
        );
344
    }
345
346
    private function logDenyRA(string $message, string $commandName, ?string $identityId): void
0 ignored issues
show
Coding Style introduced by
Private method name "CommandAuthorizationService::logDenyRA" must be prefixed with an underscore
Loading history...
347
    {
348
        if (!$identityId) {
349
            $identityId = '"unknown identityId"';
350
        }
351
        $this->logger->error(
352
            sprintf(
353
                'Denying RA command %s for identity %s. With message "%s"',
354
                $commandName,
355
                $identityId,
356
                $message
357
            )
358
        );
359
    }
360
}
361