Passed
Push — master ( 207765...57309d )
by Rutger
03:12
created

Oauth2Client::setEndUsersMayAuthorizeClient()   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\interfaces\models\Oauth2ClientInterface;
7
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientScopeInterface;
8
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface;
9
use rhertogh\Yii2Oauth2Server\models\behaviors\DateTimeBehavior;
10
use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EnabledTrait;
11
use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EntityIdentifierTrait;
12
use rhertogh\Yii2Oauth2Server\Oauth2Module;
13
use Yii;
14
use yii\base\Exception;
15
use yii\base\InvalidArgumentException;
16
use yii\base\InvalidConfigException;
17
use yii\base\UnknownPropertyException;
18
use yii\helpers\ArrayHelper;
19
use yii\helpers\Json;
20
21
class Oauth2Client extends base\Oauth2Client implements Oauth2ClientInterface
22
{
23
    use Oauth2EntityIdentifierTrait;
24
    use Oauth2EnabledTrait;
25
26
    protected const ENCRYPTED_ATTRIBUTES = ['secret', 'old_secret'];
27
28
    /**
29
     * Minimum length for new client secrets.
30
     * @var int
31
     */
32
    protected $minimumSecretLength = 10;
33
34
    /////////////////////////////
35
    /// ActiveRecord Settings ///
36
    /////////////////////////////
37
38
    /**
39
     * @inheritDoc
40
     */
41 60
    public function behaviors()
42
    {
43 60
        return ArrayHelper::merge(parent::behaviors(), [
44 60
            'dateTimeBehavior' => DateTimeBehavior::class
45 60
        ]);
46
    }
47
48
    /**
49
     * @inheritDoc
50
     */
51 1
    public function rules()
52
    {
53 1
        return ArrayHelper::merge(parent::rules(), [
54 1
            [
55 1
                ['secret'],
56 1
                'required',
57 1
                'when' => fn(self $model) => $model->isConfidential(),
58 1
            ],
59 1
            [
60 1
                ['scope_access'],
61 1
                'in',
62 1
                'range' => static::SCOPE_ACCESSES,
63 1
            ]
64 1
        ]);
65
    }
66
67
    /////////////////////////
68
    /// Getters & Setters ///
69
    /////////////////////////
70
71
    /**
72
     * @inheritdoc
73
     */
74 61
    public function __set($name, $value)
75
    {
76 61
        if ($name === 'secret') { // Don't allow setting the secret via magic method.
77 1
            throw new UnknownPropertyException('For security the "secret" property must be set via setSecret()');
78
        } else {
79 60
            parent::__set($name, $value);
80
        }
81
    }
82
83
    /**
84
     * @inheritdoc
85
     */
86 4
    public function getName()
87
    {
88 4
        return $this->name;
89
    }
90
91
    /**
92
     * @inheritdoc
93
     */
94 1
    public function setName($name)
95
    {
96 1
        $this->name = $name;
97 1
        return $this;
98
    }
99
100
    /**
101
     * @inheritdoc
102
     */
103 1
    public function getType()
104
    {
105 1
        return $this->type;
106
    }
107
108
    /**
109
     * @inheritdoc
110
     */
111 2
    public function setType($type)
112
    {
113 2
        if (!in_array($type, Oauth2ClientInterface::TYPES)) {
114 1
            throw new InvalidArgumentException('Unknown type "' . $type . '".');
115
        }
116
117 1
        $this->type = $type;
118 1
        return $this;
119
    }
120
121
    /**
122
     * @inheritdoc
123
     * @throws InvalidConfigException
124
     */
125 9
    public function getRedirectUri()
126
    {
127 9
        $uris = $this->redirect_uris;
128 9
        if (empty($uris)) {
129 1
            return [];
130
        }
131
132 8
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
133
            try {
134 5
                $uris = Json::decode($uris);
135 1
            } catch (InvalidArgumentException $e) {
136 1
                throw new InvalidConfigException('Invalid json in redirect_uris for client ' . $this->id, 0, $e);
137
            }
138
        }
139
140 7
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
141 1
            $uris = [$uris];
142 6
        } elseif (is_array($uris)) {
0 ignored issues
show
introduced by
The condition is_array($uris) is always true.
Loading history...
143 5
            $uris = array_values($uris);
144
        } else {
145 1
            throw new InvalidConfigException('`redirect_uris` must be a JSON encoded string or array of strings.');
146
        }
147
148 6
        foreach ($uris as $key => $uri) {
149 6
            if (!is_string($uri)) {
150 1
                throw new InvalidConfigException('`redirect_uris` must be a JSON encoded string or array of strings.');
151
            }
152 5
            preg_match_all('/\${(?<name>[a-zA-Z0-9_]+)}/', $uri, $matches, PREG_SET_ORDER);
153 5
            foreach ($matches as $match) {
154 1
                $envVar = getenv($match['name']);
155 1
                if (strlen($envVar)) {
156 1
                    $uris[$key] = str_replace($match[0], $envVar, $uris[$key]);
157
                } else {
158 1
                    unset($uris[$key]);
159 1
                    break;
160
                }
161
            }
162
        }
163 5
        return array_values($uris); // Re-index array in case elements were removed.
164
    }
165
166
    /**
167
     * @inheritDoc
168
     */
169 4
    public function setRedirectUri($uri)
170
    {
171 4
        if (is_array($uri)) {
172 2
            foreach ($uri as $value) {
173 2
                if (!is_string($value)) {
174 1
                    throw new InvalidArgumentException('When $uri is an array, its values must be strings.');
175
                }
176
            }
177 1
            $uri = Json::encode($uri);
178 2
        } elseif (is_string($uri)) {
179 1
            $uri = Json::encode([$uri]);
180
        } else {
181 1
            throw new InvalidArgumentException('$uri must be a string or an array, got: ' . gettype($uri));
182
        }
183
184 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...
185
186 2
        return $this;
187
    }
188
189 2
    public function isVariableRedirectUriQueryAllowed()
190
    {
191 2
        return (bool)$this->allow_variable_redirect_uri_query;
192
    }
193 1
    public function setAllowVariableRedirectUriQuery($allowVariableRedirectUriQuery)
194
    {
195 1
        $this->allow_variable_redirect_uri_query = $allowVariableRedirectUriQuery;
196 1
        return $this;
197
    }
198
199
    /**
200
     * @inheritDoc
201
     */
202 4
    public function getUserAccountSelection()
203
    {
204 4
        return $this->user_account_selection;
205
    }
206
207 4
    public function endUsersMayAuthorizeClient()
208
    {
209 4
        return $this->end_users_may_authorize_client;
210
    }
211
212 1
    public function setEndUsersMayAuthorizeClient($endUsersMayAuthorizeClient)
213
    {
214 1
        $this->end_users_may_authorize_client = $endUsersMayAuthorizeClient;
215 1
        return $this;
216
    }
217
218
    /**
219
     * @inheritDoc
220
     */
221 1
    public function setUserAccountSelection($userAccountSelectionConfig)
222
    {
223 1
        $this->user_account_selection = $userAccountSelectionConfig;
224 1
        return $this;
225
    }
226
227
    /**
228
     * @inheritDoc
229
     */
230 4
    public function isAuthCodeWithoutPkceAllowed()
231
    {
232 4
        return (bool)$this->allow_auth_code_without_pkce;
233
    }
234
235
    /**
236
     * @inheritDoc
237
     */
238 1
    public function setAllowAuthCodeWithoutPkce($allowAuthCodeWithoutPkce)
239
    {
240 1
        $this->allow_auth_code_without_pkce = $allowAuthCodeWithoutPkce;
241 1
        return $this;
242
    }
243
244
    /**
245
     * @inheritDoc
246
     */
247 2
    public function skipAuthorizationIfScopeIsAllowed()
248
    {
249 2
        return (bool)$this->skip_authorization_if_scope_is_allowed;
250
    }
251
252
    /**
253
     * @inheritDoc
254
     */
255 1
    public function setSkipAuthorizationIfScopeIsAllowed($skipAuthIfScopeIsAllowed)
256
    {
257 1
        $this->skip_authorization_if_scope_is_allowed = $skipAuthIfScopeIsAllowed;
258 1
        return $this;
259
    }
260
261
    /**
262
     * @inheritDoc
263
     */
264 1
    public function getClientCredentialsGrantUserId()
265
    {
266 1
        return $this->client_credentials_grant_user_id;
267
    }
268
269
    /**
270
     * @inheritDoc
271
     */
272 1
    public function setClientCredentialsGrantUserId($userId)
273
    {
274 1
        $this->client_credentials_grant_user_id = $userId;
275 1
        return $this;
276
    }
277
278
    /**
279
     * @inheritDoc
280
     */
281 1
    public function getOpenIdConnectAllowOfflineAccessWithoutConsent()
282
    {
283 1
        return (bool)$this->oidc_allow_offline_access_without_consent;
284
    }
285
286
    /**
287
     * @inheritDoc
288
     */
289 1
    public function setOpenIdConnectAllowOfflineAccessWithoutConsent($allowOfflineAccessWithoutConsent)
290
    {
291 1
        $this->oidc_allow_offline_access_without_consent = $allowOfflineAccessWithoutConsent;
292 1
        return $this;
293
    }
294
295
    /**
296
     * @inheritDoc
297
     */
298 1
    public function getOpenIdConnectUserinfoEncryptedResponseAlg()
299
    {
300 1
        return $this->oidc_userinfo_encrypted_response_alg;
301
    }
302
303
    /**
304
     * @inheritDoc
305
     */
306 1
    public function setOpenIdConnectUserinfoEncryptedResponseAlg($algorithm)
307
    {
308 1
        $this->oidc_userinfo_encrypted_response_alg = $algorithm;
309 1
        return $this;
310
    }
311
312
    /**
313
     * @inheritdoc
314
     */
315 9
    public function isConfidential()
316
    {
317 9
        return (int)$this->type !== static::TYPE_PUBLIC;
318
    }
319
320
    /**
321
     * @inheritDoc
322
     */
323 19
    public function getScopeAccess()
324
    {
325 19
        return (int)$this->scope_access;
326
    }
327
328
    /**
329
     * @inheritDoc
330
     */
331 2
    public function setScopeAccess($scopeAccess)
332
    {
333 2
        if (!in_array($scopeAccess, Oauth2ClientInterface::SCOPE_ACCESSES)) {
334 1
            throw new InvalidArgumentException('Unknown scope access "' . $scopeAccess . '".');
335
        }
336
337 1
        $this->scope_access = $scopeAccess;
338 1
        return $this;
339
    }
340
341
    /**
342
     * @inheritDoc
343
     */
344 3
    public static function getEncryptedAttributes()
345
    {
346 3
        return static::ENCRYPTED_ATTRIBUTES;
347
    }
348
349
    /**
350
     * @inheritDoc
351
     */
352 2
    public static function rotateStorageEncryptionKeys($encryptor, $newKeyName = null)
353
    {
354 2
        $numUpdated = 0;
355 2
        $encryptedAttributes = static::getEncryptedAttributes();
356 2
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
357
358 2
        $transaction = static::getDb()->beginTransaction();
359
        try {
360
            /** @var static $client */
361 2
            foreach ($query->each() as $client) {
362 2
                $client->rotateStorageEncryptionKey($encryptor, $newKeyName);
363 2
                if ($client->getDirtyAttributes($encryptedAttributes)) {
364 2
                    $client->persist();
365 1
                    $numUpdated++;
366
                }
367
            }
368 1
            $transaction->commit();
369 1
        } catch (\Exception $e) {
370 1
            $transaction->rollBack();
371 1
            throw $e;
372
        }
373
374 1
        return $numUpdated;
375
    }
376
377
    /**
378
     * @inheritDoc
379
     */
380
    public static function getUsedStorageEncryptionKeys($encryptor)
381
    {
382
        $encryptedAttributes = static::getEncryptedAttributes();
383
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
384
385
        $keyUsage = [];
386
        foreach ($query->each() as $client) {
387
            foreach ($encryptedAttributes as $encryptedAttribute) {
388
                $data = $client->$encryptedAttribute;
389
                if (!empty($data)) {
390
                    ['keyName' => $keyName] = $encryptor->parseData($data);
391
                    if (array_key_exists($keyName, $keyUsage)) {
392
                        $keyUsage[$keyName][] = $client->getPrimaryKey();
393
                    } else {
394
                        $keyUsage[$keyName] = [$client->getPrimaryKey()];
395
                    }
396
                }
397
            }
398
        }
399
400
        return $keyUsage;
401
    }
402
403
    /**
404
     * @inheritDoc
405
     */
406 3
    public function rotateStorageEncryptionKey($encryptor, $newKeyName = null)
407
    {
408 3
        foreach (static::getEncryptedAttributes() as $attribute) {
409 3
            $data = $this->getAttribute($attribute);
410 3
            if ($data) {
411
                try {
412 3
                    $this->setAttribute($attribute, $encryptor->rotateKey($data, $newKeyName));
413
                } catch (\Exception $e) {
414
                    throw new Exception('Unable to rotate key for client "' . $this->identifier
415
                        . '", attribute "' . $attribute . '": ' . $e->getMessage(), 0, $e);
416
                }
417
            }
418
        }
419
    }
420
421
    /**
422
     * @inheritDoc
423
     */
424 4
    public function setSecret($secret, $encryptor, $oldSecretValidUntil = null, $keyName = null)
425
    {
426 4
        if ($this->isConfidential()) {
427 4
            if (!$this->validateNewSecret($secret, $error)) {
428 1
                throw new InvalidArgumentException($error);
429
            }
430
431
            // Ensure we clear out any old secret.
432 3
            $this->setAttribute('old_secret', null);
433 3
            $this->setAttribute('old_secret_valid_until', null);
434
435 3
            if ($oldSecretValidUntil) {
436 1
                $oldSecretData = $this->getAttribute('secret') ?? null;
437 1
                if ($oldSecretData) {
438
                    // Ensure correct encryption key.
439 1
                    $oldSecretData = $encryptor->encryp($encryptor->decrypt($oldSecretData), $keyName);
440 1
                    $this->setAttribute('old_secret', $oldSecretData);
441
442 1
                    if ($oldSecretValidUntil instanceof \DateInterval) {
443 1
                        $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil);
444
                    }
445 1
                    $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil);
446
                }
447
            }
448
449 3
            $this->setAttribute('secret', $encryptor->encryp($secret, $keyName));
450
        } else {
451 1
            if ($secret !== null) {
452 1
                throw new InvalidArgumentException(
453 1
                    'The secret for a non-confidential client can only be set to `null`.'
454 1
                );
455
            }
456
457 1
            $this->setAttribute('secret', null);
458
        }
459
460 3
        return $this;
461
    }
462
463
    /**
464
     * @inheritDoc
465
     */
466 4
    public function validateNewSecret($secret, &$error)
467
    {
468 4
        $error = null;
469 4
        if (mb_strlen($secret) < $this->getMinimumSecretLength()) {
470 1
            $error = 'Secret should be at least ' . $this->getMinimumSecretLength() . ' characters.';
471
        }
472
473 4
        return $error === null;
474
    }
475
476
    /**
477
     * @inheritDoc
478
     */
479 4
    public function getMinimumSecretLength()
480
    {
481 4
        return $this->minimumSecretLength;
482
    }
483
484
    /**
485
     * @inheritDoc
486
     */
487
    public function setMinimumSecretLength($minimumSecretLength)
488
    {
489
        if (!(int)$minimumSecretLength) {
490
            throw new InvalidArgumentException('$minimumSecretLength can not be empty.');
491
        }
492
        $this->minimumSecretLength = (int)$minimumSecretLength;
493
        return $this;
494
    }
495
496
    /**
497
     * @inheritDoc
498
     */
499 2
    public function getDecryptedSecret($encryptor)
500
    {
501 2
        return $encryptor->decrypt($this->secret);
502
    }
503
504
    /**
505
     * @inheritDoc
506
     */
507
    public function getDecryptedOldSecret($encryptor)
508
    {
509
        return $encryptor->decrypt($this->old_secret);
510
    }
511
512
    /**
513
     * @inheritDoc
514
     */
515
    public function getOldSecretValidUntil()
516
    {
517
        return $this->old_secret_valid_until;
518
    }
519
520
    /**
521
     * @inheritdoc
522
     */
523 1
    public function validateSecret($secret, $encryptor)
524
    {
525 1
        return is_string($secret)
526 1
            && strlen($secret)
527 1
            && (
528 1
                Yii::$app->security->compareString($this->getDecryptedSecret($encryptor), $secret)
529 1
                || (
530 1
                    !empty($this->old_secret)
531 1
                    && !empty($this->old_secret_valid_until)
532 1
                    && $this->old_secret_valid_until > (new \DateTime())
533 1
                    && Yii::$app->security->compareString($encryptor->decrypt($this->old_secret), $secret)
534 1
                )
535 1
            );
536
    }
537
538
    /**
539
     * @inheritdoc
540
     */
541 1
    public function getLogoUri()
542
    {
543 1
        return $this->logo_uri;
544
    }
545
546
    /**
547
     * @inheritdoc
548
     */
549 1
    public function setLogoUri($logoUri)
550
    {
551 1
        $this->logo_uri = $logoUri;
552 1
        return $this;
553
    }
554
555
    /**
556
     * @inheritdoc
557
     */
558 1
    public function getTermsOfServiceUri()
559
    {
560 1
        return $this->tos_uri;
561
    }
562
563
    /**
564
     * @inheritdoc
565
     */
566 1
    public function setTermsOfServiceUri($tosUri)
567
    {
568 1
        $this->tos_uri = $tosUri;
569 1
        return $this;
570
    }
571
572
    /**
573
     * @inheritdoc
574
     */
575 1
    public function getContacts()
576
    {
577 1
        return $this->contacts;
578
    }
579
580
    /**
581
     * @inheritdoc
582
     */
583 1
    public function setContacts($contacts)
584
    {
585 1
        $this->contacts = $contacts;
586 1
        return $this;
587
    }
588
589
    /**
590
     * @inheritDoc
591
     */
592 1
    public function getGrantTypes()
593
    {
594 1
        return (int)$this->grant_types;
595
    }
596
597
    /**
598
     * @inheritDoc
599
     */
600 2
    public function setGrantTypes($grantTypes)
601
    {
602 2
        $grantTypeIds = array_flip(Oauth2Module::GRANT_TYPE_MAPPING);
603 2
        for ($i = (int)log(PHP_INT_MAX, 2); $i >= 0; $i--) {
604 2
            $grantTypeId = (int)pow(2, $i);
605 2
            if ($grantTypes & $grantTypeId) {
606 2
                if (!array_key_exists($grantTypeId, $grantTypeIds)) {
607 1
                    throw new InvalidArgumentException('Unknown Grant Type ID: ' . $grantTypeId);
608
                }
609
            }
610
        }
611
612 1
        $this->grant_types = $grantTypes;
613
614 1
        return $this;
615
    }
616
617
    /**
618
     * @inheritDoc
619
     */
620 2
    public function validateGrantType($grantTypeIdentifier)
621
    {
622 2
        $grantTypeId = Oauth2Module::getGrantTypeId($grantTypeIdentifier);
623 2
        if (empty($grantTypeId)) {
624 1
            throw new InvalidArgumentException('Unknown grant type "' . $grantTypeIdentifier . '".');
625
        }
626
627 1
        return (bool)($this->getGrantTypes() & $grantTypeId);
628
    }
629
630
    /**
631
     * @inheritDoc
632
     */
633 14
    public function validateAuthRequestScopes($scopeIdentifiers, &$unauthorizedScopes = [])
634
    {
635
        if (
636 14
            empty($scopeIdentifiers)
637
            // Quiet mode will always allow the request (scopes will silently be limited to the defined ones).
638 14
            || $this->getScopeAccess() === static::SCOPE_ACCESS_STRICT_QUIET
639
        ) {
640 4
            $unauthorizedScopes = [];
641 4
            return true;
642
        }
643
644 10
        $allowedScopeIdentifiers = array_map(
645 10
            fn($scope) => $scope->getIdentifier(),
646 10
            $this->getAllowedScopes($scopeIdentifiers)
647 10
        );
648
649 9
        $unauthorizedScopes = array_values(array_diff($scopeIdentifiers, $allowedScopeIdentifiers));
650
651 9
        return empty($unauthorizedScopes);
652
    }
653
654
    /**
655
     * @inheritDoc
656
     * @throws InvalidConfigException
657
     */
658 15
    public function getAllowedScopes($requestedScopeIdentifiers = [])
659
    {
660
        /** @var Oauth2ClientScopeInterface $clientScopeClass */
661 15
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
662 15
        $clientScopeTableName = $clientScopeClass::tableName();
663
        /** @var Oauth2ScopeInterface $scopeClass */
664 15
        $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class);
665 15
        $scopeTableName = $scopeClass::tableName();
666
667 15
        $possibleScopesConditions = [
668
            // Default scopes defined for this client.
669 15
            ['AND',
670 15
                [$clientScopeTableName . '.client_id' => $this->getPrimaryKey()],
671 15
                [$clientScopeTableName . '.enabled' => 1],
672 15
                ['OR',
673 15
                    ...(
674 15
                        !empty($requestedScopeIdentifiers)
675 12
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
676
                            : []
677 15
                    ),
678 15
                    ['NOT', [
679 15
                        $clientScopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
680 15
                    ]],
681 15
                    ['AND',
682 15
                        [$clientScopeTableName . '.applied_by_default' => null],
683 15
                        ['NOT', [
684 15
                            $scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
685 15
                        ]],
686 15
                    ],
687 15
                ],
688 15
            ],
689 15
        ];
690
691 15
        $scopeAccess = $this->getScopeAccess();
692 15
        if ($scopeAccess === Oauth2Client::SCOPE_ACCESS_PERMISSIVE) {
693
            // Default scopes defined by scope for all client.
694 4
            $possibleScopesConditions[] = ['AND',
695 4
                [$clientScopeTableName . '.client_id' => null],
696 4
                ['OR',
697 4
                    ...(
698 4
                        !empty($requestedScopeIdentifiers)
699 3
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
700
                            : []
701 4
                    ),
702 4
                    ['NOT', [$scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO]],
703 4
                ],
704 4
            ];
705
        } elseif (
706 11
            ($scopeAccess !== Oauth2Client::SCOPE_ACCESS_STRICT)
707 11
            && ($scopeAccess !== Oauth2Client::SCOPE_ACCESS_STRICT_QUIET)
708
        ) {
709
            // safeguard against unknown types.
710 1
            throw new \LogicException('Unknown scope_access: "' . $scopeAccess . '".');
711
        }
712
713 14
        return $scopeClass::find()
714 14
            ->joinWith('clientScopes', true)
715 14
            ->enabled()
716 14
            ->andWhere(['OR', ...$possibleScopesConditions])
717 14
            ->orderBy('id')
718 14
            ->all();
719
    }
720
721
    /**
722
     * @inheritdoc
723
     * @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...
724
     *     'unaffected': Oauth2ClientScopeInterface[],
725
     *     'new': Oauth2ClientScopeInterface[],
726
     *     'updated': Oauth2ClientScopeInterface[],
727
     *     'deleted': Oauth2ClientScopeInterface[],
728
     * }
729
     */
730 11
    public function syncClientScopes($scopes, $scopeRepository)
731
    {
732 11
        if (is_string($scopes)) {
733 2
            $scopes = explode(' ', $scopes);
734 9
        } elseif ($scopes === null) {
735 1
            $scopes = [];
736 8
        } elseif (!is_array($scopes)) {
737 1
            throw new InvalidArgumentException('$scopes must be a string, an array or null.');
738
        }
739
740
        /** @var class-string<Oauth2ClientScopeInterface> $clientScopeClass */
741 10
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
742
743
        /** @var Oauth2ClientScopeInterface[] $origClientScopes */
744 10
        $origClientScopes = $clientScopeClass::findAll([
745 10
            'client_id' => $this->getPrimaryKey(),
746 10
        ]);
747
748 10
        $origClientScopes = array_combine(
749 10
            array_map(
750 10
                fn(Oauth2ClientScopeInterface $clientScope) => implode('-', $clientScope->getPrimaryKey(true)),
751 10
                $origClientScopes
752 10
            ),
753 10
            $origClientScopes
754 10
        );
755
756
        /** @var Oauth2ClientScopeInterface[] $clientScopes */
757 10
        $clientScopes = [];
758
759 10
        foreach ($scopes as $key => $value) {
760 9
            if ($value instanceof Oauth2ClientScopeInterface) {
761 2
                $clientScope = $value;
762 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...
763 2
                $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
764 2
                if (array_key_exists($pkIndex, $origClientScopes)) {
765
                    // Overwrite orig (might still be considered "unchanged" when new ClientScope is not "dirty").
766 2
                    $origClientScopes[$pkIndex] = $clientScope;
767
                }
768
            } else {
769
770 7
                $scopeIdentifier = null;
771 7
                $clientScopeConfig = [
772 7
                    'client_id' => $this->getPrimaryKey(),
773 7
                ];
774
775 7
                if (is_string($value)) {
776 3
                    $scopeIdentifier = $value;
777 4
                } elseif ($value instanceof Oauth2ScopeInterface) {
778 2
                    $scopePk = $value->getPrimaryKey();
779 2
                    if ($scopePk) {
780 1
                        $clientScopeConfig = ArrayHelper::merge(
781 1
                            $clientScopeConfig,
782 1
                            ['scope_id' => $scopePk]
783 1
                        );
784
                    } else {
785
                        // New model, using identifier.
786 2
                        $scopeIdentifier = $value->getIdentifier();
787
                    }
788 2
                } elseif (is_array($value)) {
789 1
                    $clientScopeConfig = ArrayHelper::merge(
790 1
                        $clientScopeConfig,
791 1
                        $value,
792 1
                    );
793 1
                    if (empty($clientScopeConfig['scope_id'])) {
794 1
                        $scopeIdentifier = $key;
795
                    }
796
                } else {
797 1
                    throw new InvalidArgumentException(
798 1
                        'If $scopes is an array, its values must be a string, array or an instance of '
799 1
                        . Oauth2ClientScopeInterface::class . ' or ' . Oauth2ScopeInterface::class . '.'
800 1
                    );
801
                }
802
803 6
                if (isset($scopeIdentifier)) {
804 4
                    $scope = $scopeRepository->getScopeEntityByIdentifier($scopeIdentifier);
805 4
                    if (empty($scope)) {
806 1
                        throw new InvalidArgumentException('No scope with identifier "'
807 1
                            . $scopeIdentifier . '" found.');
808
                    }
809 3
                    if (!($scope instanceof Oauth2ScopeInterface)) {
810
                        throw new InvalidConfigException(get_class($scope) . ' must implement ' . Oauth2ScopeInterface::class);
811
                    }
812 3
                    $clientScopeConfig['scope_id'] = $scope->getPrimaryKey();
813
                } else {
814 3
                    if (empty($clientScopeConfig['scope_id'])) {
815 1
                        throw new InvalidArgumentException('Element ' . $key
816 1
                            . ' in $scope should specify either the scope id or its identifier.');
817
                    }
818
                }
819
820 4
                $pkIndex = $clientScopeConfig['client_id'] . '-' . $clientScopeConfig['scope_id'];
821 4
                if (array_key_exists($pkIndex, $origClientScopes)) {
822 4
                    $clientScope = $origClientScopes[$pkIndex];
823 4
                    $clientScope->setAttributes($clientScopeConfig, false);
824
                } else {
825
                    /** @var Oauth2ClientScopeInterface $clientScope */
826 4
                    $clientScope = Yii::createObject(ArrayHelper::merge(
827 4
                        ['class' => $clientScopeClass],
828 4
                        $clientScopeConfig
829 4
                    ));
830
                }
831
            }
832
833 6
            $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
834 6
            $clientScopes[$pkIndex] = $clientScope;
835
        }
836
837 7
        $transaction = static::getDb()->beginTransaction();
838
        try {
839
            // Delete records no longer present in the provided data.
840
            /** @var self[]|array[] $deleteClientScopes */
841 7
            $deleteClientScopes = array_diff_key($origClientScopes, $clientScopes);
842 7
            foreach ($deleteClientScopes as $deleteClientScope) {
843 6
                $deleteClientScope->delete();
844
            }
845
846
            // Create records not present in the provided data.
847 7
            $createClientScopes = array_diff_key($clientScopes, $origClientScopes);
848 7
            foreach ($createClientScopes as $createClientScope) {
849 6
                $createClientScope->persist();
850
            }
851
852
            // Update existing records if needed.
853 6
            $unaffectedClientScopes = [];
854 6
            $updatedClientScopes = [];
855 6
            foreach (array_intersect_key($origClientScopes, $clientScopes) as $key => $existingClientScope) {
856 5
                if ($existingClientScope->getDirtyAttributes()) {
857 2
                    $existingClientScope->persist();
858 2
                    $updatedClientScopes[$key] = $existingClientScope;
859
                } else {
860 5
                    $unaffectedClientScopes[$key] = $existingClientScope;
861
                }
862
            }
863
864 6
            $transaction->commit();
865 1
        } catch (\Exception $e) {
866 1
            $transaction->rollBack();
867 1
            throw $e;
868
        }
869
870 6
        return [
871 6
            'unaffected' => $unaffectedClientScopes,
872 6
            'new' => $createClientScopes,
873 6
            'updated' => $updatedClientScopes,
874 6
            'deleted' => $deleteClientScopes,
875 6
        ];
876
    }
877
}
878