Passed
Push — master ( 02bea5...62dfbf )
by Rutger
03:04
created

setSkipAuthorizationIfScopeIsAllowed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace rhertogh\Yii2Oauth2Server\models;
4
5
use rhertogh\Yii2Oauth2Server\helpers\DiHelper;
6
use rhertogh\Yii2Oauth2Server\helpers\EnvironmentHelper;
7
use rhertogh\Yii2Oauth2Server\helpers\exceptions\EnvironmentVariableNotAllowedException;
8
use rhertogh\Yii2Oauth2Server\helpers\exceptions\EnvironmentVariableNotSetException;
9
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
10
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientScopeInterface;
11
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface;
12
use rhertogh\Yii2Oauth2Server\models\behaviors\DateTimeBehavior;
13
use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EnabledTrait;
14
use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EntityIdentifierTrait;
15
use rhertogh\Yii2Oauth2Server\Oauth2Module;
16
use Yii;
17
use yii\base\Exception;
18
use yii\base\InvalidArgumentException;
19
use yii\base\InvalidConfigException;
20
use yii\base\UnknownPropertyException;
21
use yii\helpers\ArrayHelper;
22
use yii\helpers\Json;
23
24
class Oauth2Client extends base\Oauth2Client implements Oauth2ClientInterface
25
{
26
    use Oauth2EntityIdentifierTrait;
27
    use Oauth2EnabledTrait;
28
29
    protected const ENCRYPTED_ATTRIBUTES = ['secret', 'old_secret'];
30
31
    /**
32
     * Minimum length for new client secrets.
33
     * @var int
34
     */
35
    protected $minimumSecretLength = 10;
36
37
    /**
38
     * Configuration for parsing the `redirect_uris` with the EnvironmentHelper::parseEnvVars().
39
     * @var array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
40
     *          allowList: array,
41
     *          denyList: array|null,
42
     *          parseNested: bool,
43
     *          exceptionWhenNotSet: bool,
44
     *          exceptionWhenNotAllowed: bool,
45
     *      }|null
46
     * @see \rhertogh\Yii2Oauth2Server\helpers\EnvironmentHelper::parseEnvVars()
47
     */
48
    protected $redirectUriEnvVarConfig = null;
49
50
    /////////////////////////////
51
    /// ActiveRecord Settings ///
52
    /////////////////////////////
53
54
    /**
55
     * @inheritDoc
56
     */
57 66
    public function behaviors()
58
    {
59 66
        return ArrayHelper::merge(parent::behaviors(), [
60 66
            'dateTimeBehavior' => DateTimeBehavior::class
61 66
        ]);
62
    }
63
64
    /**
65
     * @inheritDoc
66
     */
67 1
    public function rules()
68
    {
69 1
        return ArrayHelper::merge(parent::rules(), [
70 1
            [
71 1
                ['secret'],
72 1
                'required',
73 1
                'when' => fn(self $model) => $model->isConfidential(),
74 1
            ],
75 1
            [
76 1
                ['scope_access'],
77 1
                'in',
78 1
                'range' => static::SCOPE_ACCESSES,
79 1
            ]
80 1
        ]);
81
    }
82
83
    /////////////////////////
84
    /// Getters & Setters ///
85
    /////////////////////////
86
87
    /**
88
     * @inheritdoc
89
     */
90 67
    public function __set($name, $value)
91
    {
92 67
        if ($name === 'secret') { // Don't allow setting the secret via magic method.
93 1
            throw new UnknownPropertyException('For security the "secret" property must be set via setSecret()');
94
        } else {
95 66
            parent::__set($name, $value);
96
        }
97
    }
98
99
    /**
100
     * @inheritdoc
101
     */
102 4
    public function getName()
103
    {
104 4
        return $this->name;
105
    }
106
107
    /**
108
     * @inheritdoc
109
     */
110 1
    public function setName($name)
111
    {
112 1
        $this->name = $name;
113 1
        return $this;
114
    }
115
116
    /**
117
     * @inheritdoc
118
     */
119 1
    public function getType()
120
    {
121 1
        return $this->type;
122
    }
123
124
    /**
125
     * @inheritdoc
126
     */
127 2
    public function setType($type)
128
    {
129 2
        if (!in_array($type, Oauth2ClientInterface::TYPES)) {
130 1
            throw new InvalidArgumentException('Unknown type "' . $type . '".');
131
        }
132
133 1
        $this->type = $type;
134 1
        return $this;
135
    }
136
137
    /**
138
     * @inheritdoc
139
     * @throws InvalidConfigException
140
     * @throws EnvironmentVariableNotSetException
141
     * @throws EnvironmentVariableNotAllowedException
142
     */
143 15
    public function getRedirectUri()
144
    {
145 15
        $uris = $this->redirect_uris;
146 15
        if (empty($uris)) {
147 1
            return [];
148
        }
149
150
        // Compatibility with DBMSs that don't support JSON data type
0 ignored issues
show
Coding Style introduced by
Inline comments must end in full-stops, exclamation marks, or question marks
Loading history...
151 14
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
152
            try {
153 11
                $uris = Json::decode($uris);
154 1
            } catch (InvalidArgumentException $e) {
155 1
                throw new InvalidConfigException('Invalid json in redirect_uris for client ' . $this->id, 0, $e);
156
            }
157
        }
158
159 13
        $redirectUriEnvVarConfig = $this->getRedirectUriEnvVarConfig();
160 13
        if ($redirectUriEnvVarConfig && version_compare(PHP_VERSION, '8.1.0', '<')) {
161
            // PHP < 8.1 can only handle indexed array when unpacking.
162 6
            $redirectUriEnvVarConfig = array_values($redirectUriEnvVarConfig);
163
        }
164
165 13
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
166 4
            if ($redirectUriEnvVarConfig && preg_match('/^\${[a-zA-Z0-9_]+}$/', $uris)) {
167 3
                $uris = EnvironmentHelper::parseEnvVars($uris, ...$redirectUriEnvVarConfig);
168
                try {
169
                    // Try to parse the content of the environment variable(s) as JSON.
170 3
                    $uris = Json::decode($uris);
171 1
                } catch (InvalidArgumentException $e) {
172
                    // Use as plain text if it failed.
173
                }
174
            }
175
        }
176
177 13
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
178 3
            $uris = [$uris];
179 10
        } elseif (is_array($uris)) {
0 ignored issues
show
introduced by
The condition is_array($uris) is always true.
Loading history...
180 9
            $uris = array_values($uris);
181
        } else {
182 1
            throw new InvalidConfigException('`redirect_uris` must be a JSON encoded string or array of strings.');
183
        }
184
185 12
        foreach ($uris as $key => $uri) {
186 12
            if (!is_string($uri)) {
187 1
                throw new InvalidConfigException('`redirect_uris` must be a JSON encoded string or array of strings.');
188
            }
189 11
            if ($redirectUriEnvVarConfig) {
190 6
                $uris[$key] = EnvironmentHelper::parseEnvVars($uri, ...$redirectUriEnvVarConfig);
0 ignored issues
show
Bug introduced by
$redirectUriEnvVarConfig is expanded, but the parameter $allowList of rhertogh\Yii2Oauth2Serve...tHelper::parseEnvVars() does not expect variable arguments. ( Ignorable by Annotation )

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

190
                $uris[$key] = EnvironmentHelper::parseEnvVars($uri, /** @scrutinizer ignore-type */ ...$redirectUriEnvVarConfig);
Loading history...
191 5
                if (!$uris[$key]) {
192 1
                    unset($uris[$key]);
193
                }
194
            }
195
        }
196 10
        return array_values($uris); // Re-index array in case elements were removed.
197
    }
198
199
    /**
200
     * @inheritDoc
201
     */
202 4
    public function setRedirectUri($uri)
203
    {
204 4
        if (is_array($uri)) {
205 2
            foreach ($uri as $value) {
206 2
                if (!is_string($value)) {
207 1
                    throw new InvalidArgumentException('When $uri is an array, its values must be strings.');
208
                }
209
            }
210 1
            $uri = Json::encode($uri);
211 2
        } elseif (is_string($uri)) {
212 1
            $uri = Json::encode([$uri]);
213
        } else {
214 1
            throw new InvalidArgumentException('$uri must be a string or an array, got: ' . gettype($uri));
215
        }
216
217 2
        $this->redirect_uris = $uri;
0 ignored issues
show
Documentation Bug introduced by
It seems like $uri of type string is incompatible with the declared type array of property $redirect_uris.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
218
219 2
        return $this;
220
    }
221
222
    /**
223
     * @inheritdoc
224
     */
225 13
    public function getRedirectUriEnvVarConfig()
226
    {
227 13
        return $this->redirectUriEnvVarConfig;
228
    }
229
230
    /**
231
     * @inheritdoc
232
     */
233 9
    public function setRedirectUriEnvVarConfig($config)
234
    {
235 9
        $this->redirectUriEnvVarConfig = $config;
236 9
        return $this;
237
    }
238
239
240 2
    public function isVariableRedirectUriQueryAllowed()
241
    {
242 2
        return (bool)$this->allow_variable_redirect_uri_query;
243
    }
244 1
    public function setAllowVariableRedirectUriQuery($allowVariableRedirectUriQuery)
245
    {
246 1
        $this->allow_variable_redirect_uri_query = $allowVariableRedirectUriQuery;
247 1
        return $this;
248
    }
249
250
    /**
251
     * @inheritDoc
252
     */
253 4
    public function getUserAccountSelection()
254
    {
255 4
        return $this->user_account_selection;
256
    }
257
258 4
    public function endUsersMayAuthorizeClient()
259
    {
260 4
        return $this->end_users_may_authorize_client;
261
    }
262
263 1
    public function setEndUsersMayAuthorizeClient($endUsersMayAuthorizeClient)
264
    {
265 1
        $this->end_users_may_authorize_client = $endUsersMayAuthorizeClient;
266 1
        return $this;
267
    }
268
269
    /**
270
     * @inheritDoc
271
     */
272 1
    public function setUserAccountSelection($userAccountSelectionConfig)
273
    {
274 1
        $this->user_account_selection = $userAccountSelectionConfig;
275 1
        return $this;
276
    }
277
278
    /**
279
     * @inheritDoc
280
     */
281 4
    public function isAuthCodeWithoutPkceAllowed()
282
    {
283 4
        return (bool)$this->allow_auth_code_without_pkce;
284
    }
285
286
    /**
287
     * @inheritDoc
288
     */
289 1
    public function setAllowAuthCodeWithoutPkce($allowAuthCodeWithoutPkce)
290
    {
291 1
        $this->allow_auth_code_without_pkce = $allowAuthCodeWithoutPkce;
292 1
        return $this;
293
    }
294
295
    /**
296
     * @inheritDoc
297
     */
298 2
    public function skipAuthorizationIfScopeIsAllowed()
299
    {
300 2
        return (bool)$this->skip_authorization_if_scope_is_allowed;
301
    }
302
303
    /**
304
     * @inheritDoc
305
     */
306 1
    public function setSkipAuthorizationIfScopeIsAllowed($skipAuthIfScopeIsAllowed)
307
    {
308 1
        $this->skip_authorization_if_scope_is_allowed = $skipAuthIfScopeIsAllowed;
309 1
        return $this;
310
    }
311
312
    /**
313
     * @inheritDoc
314
     */
315 1
    public function getClientCredentialsGrantUserId()
316
    {
317 1
        return $this->client_credentials_grant_user_id;
318
    }
319
320
    /**
321
     * @inheritDoc
322
     */
323 1
    public function setClientCredentialsGrantUserId($userId)
324
    {
325 1
        $this->client_credentials_grant_user_id = $userId;
326 1
        return $this;
327
    }
328
329
    /**
330
     * @inheritDoc
331
     */
332 1
    public function getOpenIdConnectAllowOfflineAccessWithoutConsent()
333
    {
334 1
        return (bool)$this->oidc_allow_offline_access_without_consent;
335
    }
336
337
    /**
338
     * @inheritDoc
339
     */
340 1
    public function setOpenIdConnectAllowOfflineAccessWithoutConsent($allowOfflineAccessWithoutConsent)
341
    {
342 1
        $this->oidc_allow_offline_access_without_consent = $allowOfflineAccessWithoutConsent;
343 1
        return $this;
344
    }
345
346
    /**
347
     * @inheritDoc
348
     */
349 1
    public function getOpenIdConnectUserinfoEncryptedResponseAlg()
350
    {
351 1
        return $this->oidc_userinfo_encrypted_response_alg;
352
    }
353
354
    /**
355
     * @inheritDoc
356
     */
357 1
    public function setOpenIdConnectUserinfoEncryptedResponseAlg($algorithm)
358
    {
359 1
        $this->oidc_userinfo_encrypted_response_alg = $algorithm;
360 1
        return $this;
361
    }
362
363
    /**
364
     * @inheritdoc
365
     */
366 9
    public function isConfidential()
367
    {
368 9
        return (int)$this->type !== static::TYPE_PUBLIC;
369
    }
370
371
    /**
372
     * @inheritDoc
373
     */
374 19
    public function getScopeAccess()
375
    {
376 19
        return (int)$this->scope_access;
377
    }
378
379
    /**
380
     * @inheritDoc
381
     */
382 2
    public function setScopeAccess($scopeAccess)
383
    {
384 2
        if (!in_array($scopeAccess, Oauth2ClientInterface::SCOPE_ACCESSES)) {
385 1
            throw new InvalidArgumentException('Unknown scope access "' . $scopeAccess . '".');
386
        }
387
388 1
        $this->scope_access = $scopeAccess;
389 1
        return $this;
390
    }
391
392
    /**
393
     * @inheritDoc
394
     */
395 3
    public static function getEncryptedAttributes()
396
    {
397 3
        return static::ENCRYPTED_ATTRIBUTES;
398
    }
399
400
    /**
401
     * @inheritDoc
402
     */
403 2
    public static function rotateStorageEncryptionKeys($cryptographer, $newKeyName = null)
404
    {
405 2
        $numUpdated = 0;
406 2
        $encryptedAttributes = static::getEncryptedAttributes();
407 2
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
408
409 2
        $transaction = static::getDb()->beginTransaction();
410
        try {
411
            /** @var static $client */
412 2
            foreach ($query->each() as $client) {
413 2
                $client->rotateStorageEncryptionKey($cryptographer, $newKeyName);
414 2
                if ($client->getDirtyAttributes($encryptedAttributes)) {
415 2
                    $client->persist();
416 1
                    $numUpdated++;
417
                }
418
            }
419 1
            $transaction->commit();
420 1
        } catch (\Exception $e) {
421 1
            $transaction->rollBack();
422 1
            throw $e;
423
        }
424
425 1
        return $numUpdated;
426
    }
427
428
    /**
429
     * @inheritDoc
430
     */
431
    public static function getUsedStorageEncryptionKeys($cryptographer)
432
    {
433
        $encryptedAttributes = static::getEncryptedAttributes();
434
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
435
436
        $keyUsage = [];
437
        foreach ($query->each() as $client) {
438
            foreach ($encryptedAttributes as $encryptedAttribute) {
439
                $data = $client->$encryptedAttribute;
440
                if (!empty($data)) {
441
                    ['keyName' => $keyName] = $cryptographer->parseData($data);
442
                    if (array_key_exists($keyName, $keyUsage)) {
443
                        $keyUsage[$keyName][] = $client->getPrimaryKey();
444
                    } else {
445
                        $keyUsage[$keyName] = [$client->getPrimaryKey()];
446
                    }
447
                }
448
            }
449
        }
450
451
        return $keyUsage;
452
    }
453
454
    /**
455
     * @inheritDoc
456
     */
457 3
    public function rotateStorageEncryptionKey($cryptographer, $newKeyName = null)
458
    {
459 3
        foreach (static::getEncryptedAttributes() as $attribute) {
460 3
            $data = $this->getAttribute($attribute);
461 3
            if ($data) {
462
                try {
463 3
                    $this->setAttribute($attribute, $cryptographer->rotateKey($data, $newKeyName));
464
                } catch (\Exception $e) {
465
                    throw new Exception('Unable to rotate key for client "' . $this->identifier
466
                        . '", attribute "' . $attribute . '": ' . $e->getMessage(), 0, $e);
467
                }
468
            }
469
        }
470
    }
471
472
    /**
473
     * @inheritDoc
474
     */
475 4
    public function setSecret($secret, $cryptographer, $oldSecretValidUntil = null, $keyName = null)
476
    {
477 4
        if ($this->isConfidential()) {
478 4
            if (!$this->validateNewSecret($secret, $error)) {
479 1
                throw new InvalidArgumentException($error);
480
            }
481
482
            // Ensure we clear out any old secret.
483 3
            $this->setAttribute('old_secret', null);
484 3
            $this->setAttribute('old_secret_valid_until', null);
485
486 3
            if ($oldSecretValidUntil) {
487 1
                $oldSecretData = $this->getAttribute('secret') ?? null;
488 1
                if ($oldSecretData) {
489
                    // Ensure correct encryption key.
490 1
                    $oldSecretData = $cryptographer->encryp($cryptographer->decrypt($oldSecretData), $keyName);
491 1
                    $this->setAttribute('old_secret', $oldSecretData);
492
493 1
                    if ($oldSecretValidUntil instanceof \DateInterval) {
494 1
                        $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil);
495
                    }
496 1
                    $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil);
497
                }
498
            }
499
500 3
            $this->setAttribute('secret', $cryptographer->encryp($secret, $keyName));
501
        } else {
502 1
            if ($secret !== null) {
503 1
                throw new InvalidArgumentException(
504 1
                    'The secret for a non-confidential client can only be set to `null`.'
505 1
                );
506
            }
507
508 1
            $this->setAttribute('secret', null);
509
        }
510
511 3
        return $this;
512
    }
513
514
    /**
515
     * @inheritDoc
516
     */
517 4
    public function validateNewSecret($secret, &$error)
518
    {
519 4
        $error = null;
520 4
        if (mb_strlen($secret) < $this->getMinimumSecretLength()) {
521 1
            $error = 'Secret should be at least ' . $this->getMinimumSecretLength() . ' characters.';
522
        }
523
524 4
        return $error === null;
525
    }
526
527
    /**
528
     * @inheritDoc
529
     */
530 5
    public function getMinimumSecretLength()
531
    {
532 5
        return $this->minimumSecretLength;
533
    }
534
535
    /**
536
     * @inheritDoc
537
     */
538 1
    public function setMinimumSecretLength($minimumSecretLength)
539
    {
540 1
        if (!(int)$minimumSecretLength) {
541
            throw new InvalidArgumentException('$minimumSecretLength can not be empty.');
542
        }
543 1
        $this->minimumSecretLength = (int)$minimumSecretLength;
544 1
        return $this;
545
    }
546
547
    /**
548
     * @inheritDoc
549
     */
550 2
    public function getDecryptedSecret($cryptographer)
551
    {
552 2
        return $cryptographer->decrypt($this->secret);
553
    }
554
555
    /**
556
     * @inheritDoc
557
     */
558
    public function getDecryptedOldSecret($cryptographer)
559
    {
560
        return $cryptographer->decrypt($this->old_secret);
561
    }
562
563
    /**
564
     * @inheritDoc
565
     */
566
    public function getOldSecretValidUntil()
567
    {
568
        return $this->old_secret_valid_until;
569
    }
570
571
    /**
572
     * @inheritdoc
573
     */
574 1
    public function validateSecret($secret, $cryptographer)
575
    {
576 1
        return is_string($secret)
577 1
            && strlen($secret)
578 1
            && (
579 1
                Yii::$app->security->compareString($this->getDecryptedSecret($cryptographer), $secret)
580 1
                || (
581 1
                    !empty($this->old_secret)
582 1
                    && !empty($this->old_secret_valid_until)
583 1
                    && $this->old_secret_valid_until > (new \DateTime())
584 1
                    && Yii::$app->security->compareString($cryptographer->decrypt($this->old_secret), $secret)
585 1
                )
586 1
            );
587
    }
588
589
    /**
590
     * @inheritdoc
591
     */
592 1
    public function getLogoUri()
593
    {
594 1
        return $this->logo_uri;
595
    }
596
597
    /**
598
     * @inheritdoc
599
     */
600 1
    public function setLogoUri($logoUri)
601
    {
602 1
        $this->logo_uri = $logoUri;
603 1
        return $this;
604
    }
605
606
    /**
607
     * @inheritdoc
608
     */
609 1
    public function getTermsOfServiceUri()
610
    {
611 1
        return $this->tos_uri;
612
    }
613
614
    /**
615
     * @inheritdoc
616
     */
617 1
    public function setTermsOfServiceUri($tosUri)
618
    {
619 1
        $this->tos_uri = $tosUri;
620 1
        return $this;
621
    }
622
623
    /**
624
     * @inheritdoc
625
     */
626 1
    public function getContacts()
627
    {
628 1
        return $this->contacts;
629
    }
630
631
    /**
632
     * @inheritdoc
633
     */
634 1
    public function setContacts($contacts)
635
    {
636 1
        $this->contacts = $contacts;
637 1
        return $this;
638
    }
639
640
    /**
641
     * @inheritDoc
642
     */
643 1
    public function getGrantTypes()
644
    {
645 1
        return (int)$this->grant_types;
646
    }
647
648
    /**
649
     * @inheritDoc
650
     */
651 2
    public function setGrantTypes($grantTypes)
652
    {
653 2
        $grantTypeIds = array_flip(Oauth2Module::GRANT_TYPE_MAPPING);
654 2
        for ($i = (int)log(PHP_INT_MAX, 2); $i >= 0; $i--) {
655 2
            $grantTypeId = (int)pow(2, $i);
656 2
            if ($grantTypes & $grantTypeId) {
657 2
                if (!array_key_exists($grantTypeId, $grantTypeIds)) {
658 1
                    throw new InvalidArgumentException('Unknown Grant Type ID: ' . $grantTypeId);
659
                }
660
            }
661
        }
662
663 1
        $this->grant_types = $grantTypes;
664
665 1
        return $this;
666
    }
667
668
    /**
669
     * @inheritDoc
670
     */
671 2
    public function validateGrantType($grantTypeIdentifier)
672
    {
673 2
        $grantTypeId = Oauth2Module::getGrantTypeId($grantTypeIdentifier);
674 2
        if (empty($grantTypeId)) {
675 1
            throw new InvalidArgumentException('Unknown grant type "' . $grantTypeIdentifier . '".');
676
        }
677
678 1
        return (bool)($this->getGrantTypes() & $grantTypeId);
679
    }
680
681
    /**
682
     * @inheritDoc
683
     */
684 14
    public function validateAuthRequestScopes($scopeIdentifiers, &$unauthorizedScopes = [])
685
    {
686
        if (
687 14
            empty($scopeIdentifiers)
688
            // Quiet mode will always allow the request (scopes will silently be limited to the defined ones).
689 14
            || $this->getScopeAccess() === static::SCOPE_ACCESS_STRICT_QUIET
690
        ) {
691 4
            $unauthorizedScopes = [];
692 4
            return true;
693
        }
694
695 10
        $allowedScopeIdentifiers = array_map(
696 10
            fn($scope) => $scope->getIdentifier(),
697 10
            $this->getAllowedScopes($scopeIdentifiers)
698 10
        );
699
700 9
        $unauthorizedScopes = array_values(array_diff($scopeIdentifiers, $allowedScopeIdentifiers));
701
702 9
        return empty($unauthorizedScopes);
703
    }
704
705
    /**
706
     * @inheritDoc
707
     * @throws InvalidConfigException
708
     */
709 15
    public function getAllowedScopes($requestedScopeIdentifiers = [])
710
    {
711
        /** @var Oauth2ClientScopeInterface $clientScopeClass */
712 15
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
713 15
        $clientScopeTableName = $clientScopeClass::tableName();
714
        /** @var Oauth2ScopeInterface $scopeClass */
715 15
        $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class);
716 15
        $scopeTableName = $scopeClass::tableName();
717
718 15
        $possibleScopesConditions = [
719
            // Default scopes defined for this client.
720 15
            ['AND',
721 15
                [$clientScopeTableName . '.client_id' => $this->getPrimaryKey()],
722 15
                [$clientScopeTableName . '.enabled' => 1],
723 15
                ['OR',
724 15
                    ...(
725 15
                        !empty($requestedScopeIdentifiers)
726 12
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
727
                            : []
728 15
                    ),
729 15
                    ['NOT', [
730 15
                        $clientScopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
731 15
                    ]],
732 15
                    ['AND',
733 15
                        [$clientScopeTableName . '.applied_by_default' => null],
734 15
                        ['NOT', [
735 15
                            $scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
736 15
                        ]],
737 15
                    ],
738 15
                ],
739 15
            ],
740 15
        ];
741
742 15
        $scopeAccess = $this->getScopeAccess();
743 15
        if ($scopeAccess === Oauth2Client::SCOPE_ACCESS_PERMISSIVE) {
744
            // Default scopes defined by scope for all client.
745 4
            $possibleScopesConditions[] = ['AND',
746 4
                [$clientScopeTableName . '.client_id' => null],
747 4
                ['OR',
748 4
                    ...(
749 4
                        !empty($requestedScopeIdentifiers)
750 3
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
751
                            : []
752 4
                    ),
753 4
                    ['NOT', [$scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO]],
754 4
                ],
755 4
            ];
756
        } elseif (
757 11
            ($scopeAccess !== Oauth2Client::SCOPE_ACCESS_STRICT)
758 11
            && ($scopeAccess !== Oauth2Client::SCOPE_ACCESS_STRICT_QUIET)
759
        ) {
760
            // safeguard against unknown types.
761 1
            throw new \LogicException('Unknown scope_access: "' . $scopeAccess . '".');
762
        }
763
764 14
        return $scopeClass::find()
765 14
            ->joinWith('clientScopes', true)
766 14
            ->enabled()
767 14
            ->andWhere(['OR', ...$possibleScopesConditions])
768 14
            ->orderBy('id')
769 14
            ->all();
770
    }
771
772
    /**
773
     * @inheritdoc
774
     * @return array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
775
     *     'unaffected': Oauth2ClientScopeInterface[],
776
     *     'new': Oauth2ClientScopeInterface[],
777
     *     'updated': Oauth2ClientScopeInterface[],
778
     *     'deleted': Oauth2ClientScopeInterface[],
779
     * }
780
     */
781 11
    public function syncClientScopes($scopes, $scopeRepository)
782
    {
783 11
        if (is_string($scopes)) {
784 2
            $scopes = explode(' ', $scopes);
785 9
        } elseif ($scopes === null) {
786 1
            $scopes = [];
787 8
        } elseif (!is_array($scopes)) {
788 1
            throw new InvalidArgumentException('$scopes must be a string, an array or null.');
789
        }
790
791
        /** @var class-string<Oauth2ClientScopeInterface> $clientScopeClass */
792 10
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
793
794
        /** @var Oauth2ClientScopeInterface[] $origClientScopes */
795 10
        $origClientScopes = $clientScopeClass::findAll([
796 10
            'client_id' => $this->getPrimaryKey(),
797 10
        ]);
798
799 10
        $origClientScopes = array_combine(
800 10
            array_map(
801 10
                fn(Oauth2ClientScopeInterface $clientScope) => implode('-', $clientScope->getPrimaryKey(true)),
802 10
                $origClientScopes
803 10
            ),
804 10
            $origClientScopes
805 10
        );
806
807
        /** @var Oauth2ClientScopeInterface[] $clientScopes */
808 10
        $clientScopes = [];
809
810 10
        foreach ($scopes as $key => $value) {
811 9
            if ($value instanceof Oauth2ClientScopeInterface) {
812 2
                $clientScope = $value;
813 2
                $clientScope->client_id = $this->getPrimaryKey(); // Ensure PK is set.
0 ignored issues
show
Bug introduced by
Accessing client_id on the interface rhertogh\Yii2Oauth2Serve...th2ClientScopeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
814 2
                $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
815 2
                if (array_key_exists($pkIndex, $origClientScopes)) {
816
                    // Overwrite orig (might still be considered "unchanged" when new ClientScope is not "dirty").
817 2
                    $origClientScopes[$pkIndex] = $clientScope;
818
                }
819
            } else {
820
821 7
                $scopeIdentifier = null;
822 7
                $clientScopeConfig = [
823 7
                    'client_id' => $this->getPrimaryKey(),
824 7
                ];
825
826 7
                if (is_string($value)) {
827 3
                    $scopeIdentifier = $value;
828 4
                } elseif ($value instanceof Oauth2ScopeInterface) {
829 2
                    $scopePk = $value->getPrimaryKey();
830 2
                    if ($scopePk) {
831 1
                        $clientScopeConfig = ArrayHelper::merge(
832 1
                            $clientScopeConfig,
833 1
                            ['scope_id' => $scopePk]
834 1
                        );
835
                    } else {
836
                        // New model, using identifier.
837 2
                        $scopeIdentifier = $value->getIdentifier();
838
                    }
839 2
                } elseif (is_array($value)) {
840 1
                    $clientScopeConfig = ArrayHelper::merge(
841 1
                        $clientScopeConfig,
842 1
                        $value,
843 1
                    );
844 1
                    if (empty($clientScopeConfig['scope_id'])) {
845 1
                        $scopeIdentifier = $key;
846
                    }
847
                } else {
848 1
                    throw new InvalidArgumentException(
849 1
                        'If $scopes is an array, its values must be a string, array or an instance of '
850 1
                        . Oauth2ClientScopeInterface::class . ' or ' . Oauth2ScopeInterface::class . '.'
851 1
                    );
852
                }
853
854 6
                if (isset($scopeIdentifier)) {
855 4
                    $scope = $scopeRepository->getScopeEntityByIdentifier($scopeIdentifier);
856 4
                    if (empty($scope)) {
857 1
                        throw new InvalidArgumentException('No scope with identifier "'
858 1
                            . $scopeIdentifier . '" found.');
859
                    }
860 3
                    if (!($scope instanceof Oauth2ScopeInterface)) {
861
                        throw new InvalidConfigException(get_class($scope) . ' must implement ' . Oauth2ScopeInterface::class);
862
                    }
863 3
                    $clientScopeConfig['scope_id'] = $scope->getPrimaryKey();
864
                } else {
865 3
                    if (empty($clientScopeConfig['scope_id'])) {
866 1
                        throw new InvalidArgumentException('Element ' . $key
867 1
                            . ' in $scope should specify either the scope id or its identifier.');
868
                    }
869
                }
870
871 4
                $pkIndex = $clientScopeConfig['client_id'] . '-' . $clientScopeConfig['scope_id'];
872 4
                if (array_key_exists($pkIndex, $origClientScopes)) {
873 4
                    $clientScope = $origClientScopes[$pkIndex];
874 4
                    $clientScope->setAttributes($clientScopeConfig, false);
875
                } else {
876
                    /** @var Oauth2ClientScopeInterface $clientScope */
877 4
                    $clientScope = Yii::createObject(ArrayHelper::merge(
878 4
                        ['class' => $clientScopeClass],
879 4
                        $clientScopeConfig
880 4
                    ));
881
                }
882
            }
883
884 6
            $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
885 6
            $clientScopes[$pkIndex] = $clientScope;
886
        }
887
888 7
        $transaction = static::getDb()->beginTransaction();
889
        try {
890
            // Delete records no longer present in the provided data.
891
            /** @var self[]|array[] $deleteClientScopes */
892 7
            $deleteClientScopes = array_diff_key($origClientScopes, $clientScopes);
893 7
            foreach ($deleteClientScopes as $deleteClientScope) {
894 6
                $deleteClientScope->delete();
895
            }
896
897
            // Create records not present in the provided data.
898 7
            $createClientScopes = array_diff_key($clientScopes, $origClientScopes);
899 7
            foreach ($createClientScopes as $createClientScope) {
900 6
                $createClientScope->persist();
901
            }
902
903
            // Update existing records if needed.
904 6
            $unaffectedClientScopes = [];
905 6
            $updatedClientScopes = [];
906 6
            foreach (array_intersect_key($origClientScopes, $clientScopes) as $key => $existingClientScope) {
907 5
                if ($existingClientScope->getDirtyAttributes()) {
908 2
                    $existingClientScope->persist();
909 2
                    $updatedClientScopes[$key] = $existingClientScope;
910
                } else {
911 5
                    $unaffectedClientScopes[$key] = $existingClientScope;
912
                }
913
            }
914
915 6
            $transaction->commit();
916 1
        } catch (\Exception $e) {
917 1
            $transaction->rollBack();
918 1
            throw $e;
919
        }
920
921 6
        return [
922 6
            'unaffected' => $unaffectedClientScopes,
923 6
            'new' => $createClientScopes,
924 6
            'updated' => $updatedClientScopes,
925 6
            'deleted' => $deleteClientScopes,
926 6
        ];
927
    }
928
}
929