mayRaCommandBeExecutedOnBehalfOf()   C
last analyzed

Complexity

Conditions 13
Paths 28

Size

Total Lines 116
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 68
nc 28
nop 3
dl 0
loc 116
rs 5.9915
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
0 ignored issues
show
Bug introduced by
The type Surfnet\Stepup\Identity\...gistrationAuthorityRole was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use Surfnet\StepupMiddleware\ApiBundle\Identity\Entity\Identity;
25
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\IdentityService;
26
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\WhitelistService;
27
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\Command;
28
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\RaExecutable;
29
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\SelfAsserted;
30
use Surfnet\StepupMiddleware\CommandHandlingBundle\Command\SelfServiceExecutable;
31
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\CreateIdentityCommand;
32
use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Command\ExpressLocalePreferenceCommand;
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
    public function __construct(
60
        private readonly WhitelistService $whitelistService,
61
        private readonly IdentityService $identityService,
62
        private readonly LoggerInterface $logger,
63
        private readonly AuthorizationContextService $authorizationContextService,
64
    ) {
65
    }
66
67
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $institution should have a doc-comment as per coding-style.
Loading history...
68
     * @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...
Coding Style introduced by
Doc comment for parameter $actorId does not match actual variable name $institution
Loading history...
69
     * @return bool
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
70
     */
71
    public function isInstitutionWhitelisted(Institution $institution, IdentityId $actorId = null): bool
72
    {
73
        // If the actor is SRAA all actions should be allowed
74
        if (!is_null($actorId) && $this->isSraa($actorId)) {
75
            return true;
76
        }
77
        return (bool)$this->whitelistService->isWhitelisted($institution->getInstitution());
78
    }
79
80
    public function maySelfServiceCommandBeExecutedOnBehalfOf(Command $command, IdentityId $actorId = null): bool
81
    {
82
        $commandName = $command::class;
83
        $identityId = $actorId instanceof IdentityId ? $actorId->getIdentityId() : null;
84
85
        // Assert Self Service command could be executed
86
        if ($command instanceof SelfServiceExecutable) {
87
            $this->logger->notice('Asserting a SelfService command');
88
89
            // If the actor is SRAA all actions should be allowed
90
            if ($this->isSraa($actorId)) {
91
                $this->logAllowSelfService(
92
                    'SRAA user is always allowed to record SelfService commands',
93
                    $commandName,
94
                    $identityId,
95
                );
96
                return true;
97
            }
98
99
            // Self Asserted token registration is allowed for SelfServiceExecutable commands
100
            if ($command instanceof SelfAsserted) {
101
                $this->logAllowSelfService(
102
                    'Allowing execution of a SelfAsserted command',
103
                    $commandName,
104
                    $identityId,
105
                );
106
                return true;
107
            }
108
109
            // the CreateIdentityCommand is used to create an Identity for a new user,
110
            // the UpdateIdentityCommand is used to update name or email of an identity
111
            // Both are only sent by the SS when the Identity is not logged in yet,
112
            // thus there is no Metadata::actorInstitution,
113
            if ($command instanceof CreateIdentityCommand || $command instanceof UpdateIdentityCommand) {
114
                $this->logAllowSelfService(
115
                    'Allowing execution of a CreateIdentityCommand or UpdateIdentityCommand command',
116
                    $commandName,
117
                    $identityId,
118
                );
119
                return true;
120
            }
121
122
            // Validate if the actor is the user
123
            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

123
            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...
124
                $this->logDenySelfService(
125
                    'The actor identity id does not match that of the identity id that was recorded in the command',
126
                    $commandName,
127
                    $identityId,
128
                );
129
                return false;
130
            }
131
        }
132
        return true;
133
    }
134
135
    /**
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...
136
     * @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...
137
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
138
     * @SuppressWarnings(PHPMD.NPathComplexity)
139
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
140
    public function mayRaCommandBeExecutedOnBehalfOf(
141
        Command $command,
142
        IdentityId $actorId = null,
143
        Institution $actorInstitution = null,
144
    ): bool {
145
        $commandName = $command::class;
146
        $identityId = $actorId instanceof IdentityId ? $actorId->getIdentityId() : null;
147
148
        $this->logger->notice('Running the mayRaCommandBeExecutedOnBehalfOf sequence');
149
        // Assert RA(A) specific authorizations
150
        if ($command instanceof RaExecutable) {
151
            $this->logger->notice('Asserting a RA command');
152
153
            // No additional FGA authorization is required for this shared (SS/RA) command
154
            if ($command instanceof ExpressLocalePreferenceCommand) {
155
                $this->logAllowRa(
156
                    'RA(A) is always allowed to perform the ExpressLocalePreferenceCommand',
157
                    $commandName,
158
                    $identityId,
159
                );
160
                return true;
161
            }
162
163
            // The actor metadata should be set
164
            if (is_null($actorId) || is_null($actorInstitution)) {
165
                $this->logDenyRA(
166
                    'ActorId and/or actorInstitution is missing in mayRaCommandBeExecutedOnBehalfOf',
167
                    $commandName,
168
                    $identityId,
169
                );
170
                return false;
171
            }
172
173
            // If the actor is SRAA all actions are allowed
174
            if ($this->isSraa($actorId)) {
175
                $this->logAllowRa(
176
                    'SRAA is always allowed to execute RA commands',
177
                    $commandName,
178
                    $identityId,
179
                );
180
                return true;
181
            }
182
183
            $raInstitution = $command->getRaInstitution();
184
            if (is_null($raInstitution)) {
185
                $raInstitution = $actorInstitution->getInstitution();
186
            }
187
188
            $this->logger->notice(sprintf('RA institution = %s', $raInstitution));
189
190
            $roleRequirement = RegistrationAuthorityRole::raa();
191
192
            // the VetSecondFactorCommand is used to vet a second factor for a user
193
            // the RevokeRegistrantsSecondFactorCommand is used to revoke a user's secondfactor
194
            // the RevokeRegistrantsRecoveryTokenCommand is used to revoke a user's recovery token
195
            // All three are only sent by the RA where the minimal role requirement is RA
196
            // all the other actions require RAA rights
197
            if ($command instanceof VetSecondFactorCommand ||
198
                $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...
199
                $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...
200
            ) {
201
                $this->logger->notice(
202
                    'VetSecondFactorCommand and RevokeRegistrantsSecondFactorCommand require a RA role',
203
                );
204
                $roleRequirement = RegistrationAuthorityRole::ra();
205
                // Use the institution of the identity (the user vetting or having his token revoked).
206
                $identity = $this->identityService->find($command->identityId);
207
                if (!$identity instanceof Identity) {
208
                    $this->logDenyRA(
209
                        'Unable to find the identity of the user that is being vetted, or revoked',
210
                        $commandName,
211
                        $identityId,
212
                    );
213
                    return false;
214
                }
215
                $this->logger->notice(
216
                    sprintf(
217
                        'Changed RA institution (before %s) to identity institution: %s',
218
                        $raInstitution,
219
                        $identity->institution->getInstitution(),
220
                    ),
221
                );
222
                $raInstitution = $identity->institution->getInstitution();
223
            }
224
225
            $authorizationContext = $this->authorizationContextService->buildInstitutionAuthorizationContext(
226
                $actorId,
227
                $roleRequirement,
228
            );
229
230
            $this->logger->notice(
231
                sprintf(
232
                    'Identity is authorized RA(A) role in institutions: %s',
233
                    implode(',', $authorizationContext->getInstitutions()->serialize()),
234
                ),
235
            );
236
237
            if (!$authorizationContext->getInstitutions()->contains(new Institution($raInstitution))) {
238
                $this->logDenyRA(
239
                    sprintf(
240
                        'Identity is not RA(A) for the specified RA institution, "%s". Allowed institutions: "%s"',
241
                        $raInstitution,
242
                        implode(',', $authorizationContext->getInstitutions()->serialize()),
243
                    ),
244
                    $commandName,
245
                    $identityId,
246
                );
247
                return false;
248
            }
249
        }
250
        $this->logAllowRa(
251
            'Allowed',
252
            $commandName,
253
            $identityId,
254
        );
255
        return true;
256
    }
257
258
    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...
259
    {
260
        if (is_null($actorId)) {
261
            return false;
262
        }
263
264
        $registrationAuthorityCredentials = $this->identityService->findRegistrationAuthorityCredentialsOf(
265
            $actorId->getIdentityId(),
266
        );
267
        if (!$registrationAuthorityCredentials instanceof \Surfnet\StepupMiddleware\ApiBundle\Identity\Value\RegistrationAuthorityCredentials) {
268
            return false;
269
        }
270
        return $registrationAuthorityCredentials->isSraa();
271
    }
272
273
    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...
274
    {
275
        if (!$identityId) {
276
            $identityId = '"unknown identityId"';
277
        }
278
279
        $this->logger->notice(
280
            sprintf(
281
                'Allowing SelfService command %s for identity %s. With message "%s"',
282
                $commandName,
283
                $identityId,
284
                $message,
285
            ),
286
        );
287
    }
288
289
    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...
290
    {
291
        if (!$identityId) {
292
            $identityId = '"unknown identityId"';
293
        }
294
        $this->logger->notice(
295
            sprintf(
296
                'Denying SelfService command %s for identity %s. With message "%s"',
297
                $commandName,
298
                $identityId,
299
                $message,
300
            ),
301
        );
302
    }
303
304
    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...
305
    {
306
        if (!$identityId) {
307
            $identityId = '"unknown identityId"';
308
        }
309
        $this->logger->notice(
310
            sprintf(
311
                'Allowing RA command %s for identity %s. With message "%s"',
312
                $commandName,
313
                $identityId,
314
                $message,
315
            ),
316
        );
317
    }
318
319
    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...
320
    {
321
        if (!$identityId) {
322
            $identityId = '"unknown identityId"';
323
        }
324
        $this->logger->error(
325
            sprintf(
326
                'Denying RA command %s for identity %s. With message "%s"',
327
                $commandName,
328
                $identityId,
329
                $message,
330
            ),
331
        );
332
    }
333
}
334