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\queries\Oauth2ClientScopeQuery; |
||||
14 | use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EnabledTrait; |
||||
15 | use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EntityIdentifierTrait; |
||||
16 | use rhertogh\Yii2Oauth2Server\Oauth2Module; |
||||
17 | use Yii; |
||||
18 | use yii\base\Exception; |
||||
19 | use yii\base\InvalidArgumentException; |
||||
20 | use yii\base\InvalidConfigException; |
||||
21 | use yii\base\UnknownPropertyException; |
||||
22 | use yii\helpers\ArrayHelper; |
||||
23 | use yii\helpers\Json; |
||||
24 | |||||
25 | class Oauth2Client extends base\Oauth2Client implements Oauth2ClientInterface |
||||
26 | { |
||||
27 | use Oauth2EntityIdentifierTrait; |
||||
28 | use Oauth2EnabledTrait; |
||||
29 | |||||
30 | protected const ENCRYPTED_ATTRIBUTES = ['secret', 'old_secret']; |
||||
31 | |||||
32 | /** |
||||
33 | * @var Oauth2Module |
||||
34 | */ |
||||
35 | protected $module; |
||||
36 | |||||
37 | /** |
||||
38 | * Minimum length for new client secrets. |
||||
39 | * @var int |
||||
40 | */ |
||||
41 | protected $minimumSecretLength = 10; |
||||
42 | |||||
43 | ///////////////////////////// |
||||
44 | /// ActiveRecord Settings /// |
||||
45 | ///////////////////////////// |
||||
46 | |||||
47 | /** |
||||
48 | * @inheritDoc |
||||
49 | */ |
||||
50 | 65 | public function behaviors() |
|||
51 | { |
||||
52 | 65 | return ArrayHelper::merge(parent::behaviors(), [ |
|||
53 | 65 | 'dateTimeBehavior' => DateTimeBehavior::class |
|||
54 | 65 | ]); |
|||
55 | } |
||||
56 | |||||
57 | /** |
||||
58 | * @inheritDoc |
||||
59 | */ |
||||
60 | 1 | public function rules() |
|||
61 | { |
||||
62 | 1 | return ArrayHelper::merge(parent::rules(), [ |
|||
63 | 1 | [ |
|||
64 | 1 | ['secret'], |
|||
65 | 1 | 'required', |
|||
66 | 1 | 'when' => fn(self $model) => $model->isConfidential(), |
|||
67 | 1 | ], |
|||
68 | 1 | ]); |
|||
69 | } |
||||
70 | |||||
71 | ///////////////////////// |
||||
72 | /// Getters & Setters /// |
||||
73 | ///////////////////////// |
||||
74 | |||||
75 | /** |
||||
76 | * @inheritdoc |
||||
77 | */ |
||||
78 | 66 | public function __set($name, $value) |
|||
79 | { |
||||
80 | 66 | if ($name === 'secret') { // Don't allow setting the secret via magic method. |
|||
81 | 1 | throw new UnknownPropertyException('For security the "secret" property must be set via setSecret()'); |
|||
82 | } else { |
||||
83 | 65 | parent::__set($name, $value); |
|||
84 | } |
||||
85 | } |
||||
86 | |||||
87 | /** |
||||
88 | * @inheritDoc |
||||
89 | */ |
||||
90 | 8 | public function getModule(): Oauth2Module |
|||
91 | { |
||||
92 | 8 | return $this->module; |
|||
93 | } |
||||
94 | |||||
95 | /** |
||||
96 | * @inheritDoc |
||||
97 | */ |
||||
98 | 52 | public function setModule($module) |
|||
99 | { |
||||
100 | 52 | $this->module = $module; |
|||
101 | 52 | return $this; |
|||
102 | } |
||||
103 | |||||
104 | /** |
||||
105 | * @inheritdoc |
||||
106 | */ |
||||
107 | 4 | public function getName() |
|||
108 | { |
||||
109 | 4 | return $this->name; |
|||
110 | } |
||||
111 | |||||
112 | /** |
||||
113 | * @inheritdoc |
||||
114 | */ |
||||
115 | 1 | public function setName($name) |
|||
116 | { |
||||
117 | 1 | $this->name = $name; |
|||
118 | 1 | return $this; |
|||
119 | } |
||||
120 | |||||
121 | /** |
||||
122 | * @inheritdoc |
||||
123 | */ |
||||
124 | 1 | public function getType() |
|||
125 | { |
||||
126 | 1 | return $this->type; |
|||
127 | } |
||||
128 | |||||
129 | /** |
||||
130 | * @inheritdoc |
||||
131 | */ |
||||
132 | 2 | public function setType($type) |
|||
133 | { |
||||
134 | 2 | if (!in_array($type, Oauth2ClientInterface::TYPES)) { |
|||
135 | 1 | throw new InvalidArgumentException('Unknown type "' . $type . '".'); |
|||
136 | } |
||||
137 | |||||
138 | 1 | $this->type = $type; |
|||
139 | 1 | return $this; |
|||
140 | } |
||||
141 | |||||
142 | /** |
||||
143 | * @inheritdoc |
||||
144 | * @throws InvalidConfigException |
||||
145 | * @throws EnvironmentVariableNotSetException |
||||
146 | * @throws EnvironmentVariableNotAllowedException |
||||
147 | */ |
||||
148 | 15 | public function getRedirectUri() |
|||
149 | { |
||||
150 | 15 | return $this->getUrisAttribute('redirect_uris'); |
|||
151 | } |
||||
152 | |||||
153 | /** |
||||
154 | * @inheritdoc |
||||
155 | * @throws InvalidConfigException |
||||
156 | * @throws EnvironmentVariableNotSetException |
||||
157 | * @throws EnvironmentVariableNotAllowedException |
||||
158 | */ |
||||
159 | public function getPostLogoutRedirectUris() |
||||
160 | { |
||||
161 | return $this->getUrisAttribute('post_logout_redirect_uris'); |
||||
162 | } |
||||
163 | |||||
164 | /** |
||||
165 | * @param string $attribute |
||||
166 | * @return string[] |
||||
167 | * @throws InvalidConfigException |
||||
168 | * @throws EnvironmentVariableNotSetException |
||||
169 | * @throws EnvironmentVariableNotAllowedException |
||||
170 | */ |
||||
171 | 15 | protected function getUrisAttribute($attribute) |
|||
172 | { |
||||
173 | 15 | $uris = $this->$attribute; |
|||
174 | 15 | if (empty($uris)) { |
|||
175 | 1 | return []; |
|||
176 | } |
||||
177 | |||||
178 | // Compatibility with DBMSs that don't support JSON data type. |
||||
179 | 14 | if (is_string($uris)) { |
|||
180 | try { |
||||
181 | 11 | $uris = Json::decode($uris); |
|||
182 | 1 | } catch (InvalidArgumentException $e) { |
|||
183 | 1 | throw new InvalidConfigException('Invalid json in `' . $attribute . '` for client ' . $this->id, 0, $e); |
|||
184 | } |
||||
185 | } |
||||
186 | |||||
187 | 13 | $redirectUrisEnvVarConfig = $this->getRedirectUrisEnvVarConfig(); |
|||
188 | 13 | if ($redirectUrisEnvVarConfig && version_compare(PHP_VERSION, '8.1.0', '<')) { |
|||
189 | // PHP < 8.1 can only handle indexed array when unpacking. |
||||
190 | 6 | $redirectUrisEnvVarConfig = array_values($redirectUrisEnvVarConfig); |
|||
191 | } |
||||
192 | |||||
193 | 13 | if (is_string($uris)) { |
|||
194 | 4 | if ($redirectUrisEnvVarConfig && preg_match('/^\${[a-zA-Z0-9_]+}$/', $uris)) { |
|||
195 | 3 | $uris = EnvironmentHelper::parseEnvVars($uris, ...$redirectUrisEnvVarConfig); |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
196 | try { |
||||
197 | // Try to parse the content of the environment variable(s) as JSON. |
||||
198 | 3 | $uris = Json::decode($uris); |
|||
199 | 1 | } catch (InvalidArgumentException $e) { |
|||
200 | // Use as plain text if it failed. |
||||
201 | } |
||||
202 | } |
||||
203 | } |
||||
204 | |||||
205 | 13 | if (is_string($uris)) { |
|||
206 | 3 | $uris = [$uris]; |
|||
207 | 10 | } elseif (is_array($uris)) { |
|||
208 | 9 | $uris = array_values($uris); |
|||
209 | } else { |
||||
210 | 1 | throw new InvalidConfigException('`' . $attribute . '` must be a JSON encoded string or array of strings.'); |
|||
211 | } |
||||
212 | |||||
213 | 12 | foreach ($uris as $key => $uri) { |
|||
214 | 12 | if (!is_string($uri)) { |
|||
215 | 1 | throw new InvalidConfigException('`' . $attribute . '` must be a JSON encoded string or array of strings.'); // phpcs:ignore Generic.Files.LineLength.TooLong |
|||
216 | } |
||||
217 | 11 | if ($redirectUrisEnvVarConfig) { |
|||
218 | 6 | $uris[$key] = EnvironmentHelper::parseEnvVars($uri, ...$redirectUrisEnvVarConfig); |
|||
219 | 5 | if (!$uris[$key]) { |
|||
220 | 1 | unset($uris[$key]); |
|||
221 | } |
||||
222 | } |
||||
223 | } |
||||
224 | 10 | return array_values($uris); // Re-index array in case elements were removed. |
|||
225 | } |
||||
226 | |||||
227 | /** |
||||
228 | * @inheritDoc |
||||
229 | */ |
||||
230 | 4 | public function setRedirectUri($uri) |
|||
231 | { |
||||
232 | 4 | return $this->setUrisAttribute('redirect_uris', $uri); |
|||
233 | } |
||||
234 | |||||
235 | /** |
||||
236 | * @inheritDoc |
||||
237 | */ |
||||
238 | public function setPostLogoutRedirectUris($uri) |
||||
239 | { |
||||
240 | return $this->setUrisAttribute('post_logout_redirect_uris', $uri); |
||||
241 | } |
||||
242 | |||||
243 | /** |
||||
244 | * @param string $attribute |
||||
245 | * @param string|string[]|null $uri |
||||
246 | * @return $this |
||||
247 | */ |
||||
248 | 4 | protected function setUrisAttribute($attribute, $uri) |
|||
249 | { |
||||
250 | 4 | if (is_array($uri)) { |
|||
251 | 2 | foreach ($uri as $value) { |
|||
252 | 2 | if (!is_string($value)) { |
|||
253 | 1 | throw new InvalidArgumentException('When $uri is an array, its values must be strings.'); |
|||
254 | } |
||||
255 | } |
||||
256 | 1 | $uri = Json::encode($uri); |
|||
257 | 2 | } elseif (is_string($uri)) { |
|||
258 | 1 | $uri = Json::encode([$uri]); |
|||
259 | 1 | } elseif ($uri !== null) { |
|||
0 ignored issues
–
show
|
|||||
260 | 1 | throw new InvalidArgumentException('$uri must be a string or an array, got: ' . gettype($uri)); |
|||
261 | } |
||||
262 | |||||
263 | 2 | $this->$attribute = $uri; |
|||
264 | |||||
265 | 2 | return $this; |
|||
266 | } |
||||
267 | |||||
268 | /** |
||||
269 | * @inheritdoc |
||||
270 | */ |
||||
271 | 18 | public function getEnvVarConfig() |
|||
272 | { |
||||
273 | 18 | return $this->env_var_config; |
|||
274 | } |
||||
275 | |||||
276 | /** |
||||
277 | * @inheritdoc |
||||
278 | */ |
||||
279 | 9 | public function setEnvVarConfig($config) |
|||
280 | { |
||||
281 | 9 | $this->env_var_config = $config; |
|||
282 | 9 | return $this; |
|||
283 | } |
||||
284 | |||||
285 | /** |
||||
286 | * @inheritdoc |
||||
287 | */ |
||||
288 | 14 | public function getRedirectUrisEnvVarConfig() |
|||
289 | { |
||||
290 | 14 | $envVarConfig = $this->getEnvVarConfig(); |
|||
291 | 14 | return is_array($envVarConfig) && array_key_exists('redirectUris', $envVarConfig) |
|||
292 | 7 | ? $this->getEnvVarConfig()['redirectUris'] |
|||
293 | 14 | : $this->getModule()->clientRedirectUrisEnvVarConfig; |
|||
294 | } |
||||
295 | |||||
296 | /** |
||||
297 | * @inheritdoc |
||||
298 | */ |
||||
299 | 3 | public function getSecretsEnvVarConfig() |
|||
300 | { |
||||
301 | 3 | $envVarConfig = $this->getEnvVarConfig(); |
|||
302 | 3 | return is_array($envVarConfig) && array_key_exists('secrets', $envVarConfig) |
|||
303 | 1 | ? $envVarConfig['secrets'] |
|||
304 | 3 | : null; |
|||
305 | } |
||||
306 | |||||
307 | 5 | public function isVariableRedirectUriQueryAllowed() |
|||
308 | { |
||||
309 | 5 | return (bool)$this->allow_variable_redirect_uri_query; |
|||
310 | } |
||||
311 | 1 | public function setAllowVariableRedirectUriQuery($allowVariableRedirectUriQuery) |
|||
312 | { |
||||
313 | 1 | $this->allow_variable_redirect_uri_query = $allowVariableRedirectUriQuery; |
|||
314 | 1 | return $this; |
|||
315 | } |
||||
316 | |||||
317 | /** |
||||
318 | * @inheritDoc |
||||
319 | */ |
||||
320 | 4 | public function getUserAccountSelection() |
|||
321 | { |
||||
322 | 4 | return $this->user_account_selection; |
|||
323 | } |
||||
324 | |||||
325 | 4 | public function endUsersMayAuthorizeClient() |
|||
326 | { |
||||
327 | 4 | return $this->end_users_may_authorize_client; |
|||
328 | } |
||||
329 | |||||
330 | 1 | public function setEndUsersMayAuthorizeClient($endUsersMayAuthorizeClient) |
|||
331 | { |
||||
332 | 1 | $this->end_users_may_authorize_client = $endUsersMayAuthorizeClient; |
|||
333 | 1 | return $this; |
|||
334 | } |
||||
335 | |||||
336 | /** |
||||
337 | * @inheritDoc |
||||
338 | */ |
||||
339 | 1 | public function setUserAccountSelection($userAccountSelectionConfig) |
|||
340 | { |
||||
341 | 1 | $this->user_account_selection = $userAccountSelectionConfig; |
|||
342 | 1 | return $this; |
|||
343 | } |
||||
344 | |||||
345 | /** |
||||
346 | * @inheritDoc |
||||
347 | */ |
||||
348 | 4 | public function isAuthCodeWithoutPkceAllowed() |
|||
349 | { |
||||
350 | 4 | return (bool)$this->allow_auth_code_without_pkce; |
|||
351 | } |
||||
352 | |||||
353 | /** |
||||
354 | * @inheritDoc |
||||
355 | */ |
||||
356 | 1 | public function setAllowAuthCodeWithoutPkce($allowAuthCodeWithoutPkce) |
|||
357 | { |
||||
358 | 1 | $this->allow_auth_code_without_pkce = $allowAuthCodeWithoutPkce; |
|||
359 | 1 | return $this; |
|||
360 | } |
||||
361 | |||||
362 | /** |
||||
363 | * @inheritDoc |
||||
364 | */ |
||||
365 | 2 | public function skipAuthorizationIfScopeIsAllowed() |
|||
366 | { |
||||
367 | 2 | return (bool)$this->skip_authorization_if_scope_is_allowed; |
|||
368 | } |
||||
369 | |||||
370 | /** |
||||
371 | * @inheritDoc |
||||
372 | */ |
||||
373 | 1 | public function setSkipAuthorizationIfScopeIsAllowed($skipAuthIfScopeIsAllowed) |
|||
374 | { |
||||
375 | 1 | $this->skip_authorization_if_scope_is_allowed = $skipAuthIfScopeIsAllowed; |
|||
376 | 1 | return $this; |
|||
377 | } |
||||
378 | |||||
379 | /** |
||||
380 | * @inheritDoc |
||||
381 | */ |
||||
382 | 1 | public function getClientCredentialsGrantUserId() |
|||
383 | { |
||||
384 | 1 | return $this->client_credentials_grant_user_id; |
|||
385 | } |
||||
386 | |||||
387 | /** |
||||
388 | * @inheritDoc |
||||
389 | */ |
||||
390 | 1 | public function setClientCredentialsGrantUserId($userId) |
|||
391 | { |
||||
392 | 1 | $this->client_credentials_grant_user_id = $userId; |
|||
393 | 1 | return $this; |
|||
394 | } |
||||
395 | |||||
396 | /** |
||||
397 | * @inheritDoc |
||||
398 | */ |
||||
399 | 1 | public function getOpenIdConnectAllowOfflineAccessWithoutConsent() |
|||
400 | { |
||||
401 | 1 | return (bool)$this->oidc_allow_offline_access_without_consent; |
|||
402 | } |
||||
403 | |||||
404 | /** |
||||
405 | * @inheritDoc |
||||
406 | */ |
||||
407 | 1 | public function setOpenIdConnectAllowOfflineAccessWithoutConsent($allowOfflineAccessWithoutConsent) |
|||
408 | { |
||||
409 | 1 | $this->oidc_allow_offline_access_without_consent = $allowOfflineAccessWithoutConsent; |
|||
410 | 1 | return $this; |
|||
411 | } |
||||
412 | |||||
413 | /** |
||||
414 | * @inheritDoc |
||||
415 | */ |
||||
416 | public function getOpenIdConnectRpInitiatedLogout() |
||||
417 | { |
||||
418 | return $this->oidc_rp_initiated_logout; |
||||
419 | } |
||||
420 | |||||
421 | /** |
||||
422 | * @inheritDoc |
||||
423 | */ |
||||
424 | public function setOpenIdConnectRpInitiatedLogout($skipLogoutValidation) |
||||
425 | { |
||||
426 | $this->oidc_rp_initiated_logout = $skipLogoutValidation; |
||||
427 | return $this; |
||||
428 | } |
||||
429 | |||||
430 | /** |
||||
431 | * @inheritDoc |
||||
432 | */ |
||||
433 | 1 | public function getOpenIdConnectUserinfoEncryptedResponseAlg() |
|||
434 | { |
||||
435 | 1 | return $this->oidc_userinfo_encrypted_response_alg; |
|||
436 | } |
||||
437 | |||||
438 | /** |
||||
439 | * @inheritDoc |
||||
440 | */ |
||||
441 | 1 | public function setOpenIdConnectUserinfoEncryptedResponseAlg($algorithm) |
|||
442 | { |
||||
443 | 1 | $this->oidc_userinfo_encrypted_response_alg = $algorithm; |
|||
444 | 1 | return $this; |
|||
445 | } |
||||
446 | |||||
447 | /** |
||||
448 | * @inheritdoc |
||||
449 | */ |
||||
450 | 9 | public function isConfidential() |
|||
451 | { |
||||
452 | 9 | return (int)$this->type !== static::TYPE_PUBLIC; |
|||
453 | } |
||||
454 | |||||
455 | /** |
||||
456 | * @inheritDoc |
||||
457 | */ |
||||
458 | 17 | public function getAllowGenericScopes() |
|||
459 | { |
||||
460 | 17 | return (bool)$this->allow_generic_scopes; |
|||
461 | } |
||||
462 | |||||
463 | /** |
||||
464 | * @inheritDoc |
||||
465 | */ |
||||
466 | 1 | public function setAllowGenericScopes($allowGenericScopes) |
|||
467 | { |
||||
468 | 1 | $this->allow_generic_scopes = $allowGenericScopes; |
|||
469 | 1 | return $this; |
|||
470 | } |
||||
471 | |||||
472 | /** |
||||
473 | * @inheritDoc |
||||
474 | */ |
||||
475 | 1 | public function getExceptionOnInvalidScope() |
|||
476 | { |
||||
477 | 1 | return $this->exception_on_invalid_scope; |
|||
478 | } |
||||
479 | |||||
480 | /** |
||||
481 | * @inheritDoc |
||||
482 | */ |
||||
483 | 1 | public function setExceptionOnInvalidScope($exceptionOnInvalidScope) |
|||
484 | { |
||||
485 | 1 | $this->exception_on_invalid_scope = $exceptionOnInvalidScope; |
|||
486 | 1 | return $this; |
|||
487 | } |
||||
488 | |||||
489 | /** |
||||
490 | * @inheritDoc |
||||
491 | */ |
||||
492 | 3 | public static function getEncryptedAttributes() |
|||
493 | { |
||||
494 | 3 | return static::ENCRYPTED_ATTRIBUTES; |
|||
495 | } |
||||
496 | |||||
497 | /** |
||||
498 | * @inheritDoc |
||||
499 | */ |
||||
500 | 2 | public static function rotateStorageEncryptionKeys($cryptographer, $newKeyName = null) |
|||
501 | { |
||||
502 | 2 | $numUpdated = 0; |
|||
503 | 2 | $encryptedAttributes = static::getEncryptedAttributes(); |
|||
504 | 2 | $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]); |
|||
505 | |||||
506 | 2 | $transaction = static::getDb()->beginTransaction(); |
|||
507 | try { |
||||
508 | /** @var static $client */ |
||||
509 | 2 | foreach ($query->each() as $client) { |
|||
510 | 2 | $client->rotateStorageEncryptionKey($cryptographer, $newKeyName); |
|||
511 | 2 | if ($client->getDirtyAttributes($encryptedAttributes)) { |
|||
512 | 2 | $client->persist(); |
|||
513 | 1 | $numUpdated++; |
|||
514 | } |
||||
515 | } |
||||
516 | 1 | $transaction->commit(); |
|||
517 | 1 | } catch (\Exception $e) { |
|||
518 | 1 | $transaction->rollBack(); |
|||
519 | 1 | throw $e; |
|||
520 | } |
||||
521 | |||||
522 | 1 | return $numUpdated; |
|||
523 | } |
||||
524 | |||||
525 | /** |
||||
526 | * @inheritDoc |
||||
527 | */ |
||||
528 | public static function getUsedStorageEncryptionKeys($cryptographer) |
||||
529 | { |
||||
530 | $encryptedAttributes = static::getEncryptedAttributes(); |
||||
531 | $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]); |
||||
532 | |||||
533 | $keyUsage = []; |
||||
534 | foreach ($query->each() as $client) { |
||||
535 | foreach ($encryptedAttributes as $encryptedAttribute) { |
||||
536 | $data = $client->$encryptedAttribute; |
||||
537 | if (!empty($data)) { |
||||
538 | ['keyName' => $keyName] = $cryptographer->parseData($data); |
||||
539 | if (array_key_exists($keyName, $keyUsage)) { |
||||
540 | $keyUsage[$keyName][] = $client->getPrimaryKey(); |
||||
541 | } else { |
||||
542 | $keyUsage[$keyName] = [$client->getPrimaryKey()]; |
||||
543 | } |
||||
544 | } |
||||
545 | } |
||||
546 | } |
||||
547 | |||||
548 | return $keyUsage; |
||||
549 | } |
||||
550 | |||||
551 | /** |
||||
552 | * @inheritDoc |
||||
553 | */ |
||||
554 | 3 | public function rotateStorageEncryptionKey($cryptographer, $newKeyName = null) |
|||
555 | { |
||||
556 | 3 | foreach (static::getEncryptedAttributes() as $attribute) { |
|||
557 | 3 | $data = $this->getAttribute($attribute); |
|||
558 | 3 | if ($data) { |
|||
559 | try { |
||||
560 | 3 | $this->setAttribute($attribute, $cryptographer->rotateKey($data, $newKeyName)); |
|||
561 | } catch (\Exception $e) { |
||||
562 | throw new Exception('Unable to rotate key for client "' . $this->identifier |
||||
563 | . '", attribute "' . $attribute . '": ' . $e->getMessage(), 0, $e); |
||||
564 | } |
||||
565 | } |
||||
566 | } |
||||
567 | } |
||||
568 | |||||
569 | /** |
||||
570 | * @inheritDoc |
||||
571 | */ |
||||
572 | 4 | public function setSecret($secret, $cryptographer, $oldSecretValidUntil = null, $keyName = null) |
|||
573 | { |
||||
574 | 4 | if ($this->isConfidential()) { |
|||
575 | 4 | if (!$this->validateNewSecret($secret, $error)) { |
|||
576 | 1 | throw new InvalidArgumentException($error); |
|||
577 | } |
||||
578 | |||||
579 | // Ensure we clear out any old secret. |
||||
580 | 3 | $this->setAttribute('old_secret', null); |
|||
581 | 3 | $this->setAttribute('old_secret_valid_until', null); |
|||
582 | |||||
583 | 3 | if ($oldSecretValidUntil) { |
|||
584 | 1 | $oldSecretData = $this->getAttribute('secret') ?? null; |
|||
585 | 1 | if ($oldSecretData) { |
|||
586 | // Ensure correct encryption key. |
||||
587 | 1 | $oldSecretData = $cryptographer->encryp($cryptographer->decrypt($oldSecretData), $keyName); |
|||
588 | 1 | $this->setAttribute('old_secret', $oldSecretData); |
|||
589 | |||||
590 | 1 | if ($oldSecretValidUntil instanceof \DateInterval) { |
|||
591 | 1 | $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil); |
|||
592 | } |
||||
593 | 1 | $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil); |
|||
594 | } |
||||
595 | } |
||||
596 | |||||
597 | 3 | $this->setAttribute('secret', $cryptographer->encryp($secret, $keyName)); |
|||
598 | } else { |
||||
599 | 1 | if ($secret !== null) { |
|||
600 | 1 | throw new InvalidArgumentException( |
|||
601 | 1 | 'The secret for a non-confidential client can only be set to `null`.' |
|||
602 | 1 | ); |
|||
603 | } |
||||
604 | |||||
605 | 1 | $this->setAttribute('secret', null); |
|||
606 | } |
||||
607 | |||||
608 | 3 | return $this; |
|||
609 | } |
||||
610 | |||||
611 | /** |
||||
612 | * @inheritDoc |
||||
613 | */ |
||||
614 | 1 | public function setSecretsAsEnvVars($secretEnvVarName, $oldSecretEnvVarName = null, $oldSecretValidUntil = null) |
|||
615 | { |
||||
616 | 1 | if (empty($secretEnvVarName)) { |
|||
617 | throw new InvalidArgumentException( |
||||
618 | 'Parameter $secretEnvVarName can not be empty.' |
||||
619 | ); |
||||
620 | } |
||||
621 | |||||
622 | 1 | if ($oldSecretEnvVarName) { |
|||
623 | 1 | if (!$oldSecretValidUntil) { |
|||
624 | throw new InvalidArgumentException( |
||||
625 | 'Parameter $oldSecretValidUntil must be set when $oldSecretEnvVar is set.' |
||||
626 | ); |
||||
627 | } |
||||
628 | 1 | $this->setAttribute('old_secret', '${' . $oldSecretEnvVarName . '}'); |
|||
629 | |||||
630 | 1 | if ($oldSecretValidUntil instanceof \DateInterval) { |
|||
631 | 1 | $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil); |
|||
632 | } |
||||
633 | 1 | $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil); |
|||
634 | } else { |
||||
635 | $this->setAttribute('old_secret', null); |
||||
636 | $this->setAttribute('old_secret_valid_until', null); |
||||
637 | } |
||||
638 | |||||
639 | 1 | $this->setAttribute('secret', '${' . $secretEnvVarName . '}'); |
|||
640 | |||||
641 | 1 | return $this; |
|||
642 | } |
||||
643 | |||||
644 | /** |
||||
645 | * @inheritDoc |
||||
646 | */ |
||||
647 | 4 | public function validateNewSecret($secret, &$error) |
|||
648 | { |
||||
649 | 4 | $error = null; |
|||
650 | 4 | if (mb_strlen($secret) < $this->getMinimumSecretLength()) { |
|||
651 | 1 | $error = 'Secret should be at least ' . $this->getMinimumSecretLength() . ' characters.'; |
|||
652 | } |
||||
653 | |||||
654 | 4 | return $error === null; |
|||
655 | } |
||||
656 | |||||
657 | /** |
||||
658 | * @inheritDoc |
||||
659 | */ |
||||
660 | 5 | public function getMinimumSecretLength() |
|||
661 | { |
||||
662 | 5 | return $this->minimumSecretLength; |
|||
663 | } |
||||
664 | |||||
665 | /** |
||||
666 | * @inheritDoc |
||||
667 | */ |
||||
668 | 1 | public function setMinimumSecretLength($minimumSecretLength) |
|||
669 | { |
||||
670 | 1 | if (!(int)$minimumSecretLength) { |
|||
671 | throw new InvalidArgumentException('$minimumSecretLength can not be empty.'); |
||||
672 | } |
||||
673 | 1 | $this->minimumSecretLength = (int)$minimumSecretLength; |
|||
674 | 1 | return $this; |
|||
675 | } |
||||
676 | |||||
677 | /** |
||||
678 | * @inheritDoc |
||||
679 | */ |
||||
680 | 3 | public function getDecryptedSecret($cryptographer) |
|||
681 | { |
||||
682 | 3 | return $cryptographer->decrypt($this->envVarParseSecret($this->secret)); |
|||
683 | } |
||||
684 | |||||
685 | /** |
||||
686 | * @inheritDoc |
||||
687 | */ |
||||
688 | 2 | public function getDecryptedOldSecret($cryptographer) |
|||
689 | { |
||||
690 | 2 | return $cryptographer->decrypt($this->envVarParseSecret($this->old_secret)); |
|||
691 | } |
||||
692 | |||||
693 | /** |
||||
694 | * Replaces environment variables with their values in the secret |
||||
695 | * |
||||
696 | * @param string $secret |
||||
697 | * @return string |
||||
698 | * @throws EnvironmentVariableNotAllowedException |
||||
699 | * @throws EnvironmentVariableNotSetException |
||||
700 | * @throws InvalidConfigException |
||||
701 | */ |
||||
702 | 3 | protected function envVarParseSecret($secret) |
|||
703 | { |
||||
704 | 3 | $secretsEnvVarConfig = $this->getSecretsEnvVarConfig(); |
|||
705 | 3 | if ($secretsEnvVarConfig) { |
|||
706 | 1 | if (version_compare(PHP_VERSION, '8.1.0', '<')) { |
|||
707 | // PHP < 8.1 can only handle indexed array when unpacking. |
||||
708 | 1 | $secretsEnvVarConfig = array_values($secretsEnvVarConfig); |
|||
709 | } |
||||
710 | 1 | $secret = EnvironmentHelper::parseEnvVars($secret, ...$secretsEnvVarConfig); |
|||
0 ignored issues
–
show
$secretsEnvVarConfig 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
![]() |
|||||
711 | 2 | } elseif (preg_match(EnvironmentHelper::ENV_VAR_REGEX, $secret)) { |
|||
712 | throw new InvalidConfigException('Environment variable used without env_var_config being set.'); |
||||
713 | } |
||||
714 | |||||
715 | 3 | return $secret; |
|||
716 | } |
||||
717 | |||||
718 | /** |
||||
719 | * @inheritDoc |
||||
720 | */ |
||||
721 | public function getOldSecretValidUntil() |
||||
722 | { |
||||
723 | return $this->old_secret_valid_until; |
||||
724 | } |
||||
725 | |||||
726 | /** |
||||
727 | * @inheritdoc |
||||
728 | */ |
||||
729 | 2 | public function validateSecret($secret, $cryptographer) |
|||
730 | { |
||||
731 | 2 | return is_string($secret) |
|||
732 | 2 | && strlen($secret) |
|||
733 | 2 | && ( |
|||
734 | 2 | Yii::$app->security->compareString($this->getDecryptedSecret($cryptographer), $secret) |
|||
735 | 2 | || ( |
|||
736 | 2 | !empty($this->old_secret) |
|||
737 | 2 | && !empty($this->old_secret_valid_until) |
|||
738 | 2 | && $this->old_secret_valid_until > (new \DateTime()) |
|||
739 | 2 | && Yii::$app->security->compareString($this->getDecryptedOldSecret($cryptographer), $secret) |
|||
740 | 2 | ) |
|||
741 | 2 | ); |
|||
742 | } |
||||
743 | |||||
744 | /** |
||||
745 | * @inheritdoc |
||||
746 | */ |
||||
747 | 1 | public function getLogoUri() |
|||
748 | { |
||||
749 | 1 | return $this->logo_uri; |
|||
750 | } |
||||
751 | |||||
752 | /** |
||||
753 | * @inheritdoc |
||||
754 | */ |
||||
755 | 1 | public function setLogoUri($logoUri) |
|||
756 | { |
||||
757 | 1 | $this->logo_uri = $logoUri; |
|||
758 | 1 | return $this; |
|||
759 | } |
||||
760 | |||||
761 | /** |
||||
762 | * @inheritdoc |
||||
763 | */ |
||||
764 | 1 | public function getTermsOfServiceUri() |
|||
765 | { |
||||
766 | 1 | return $this->tos_uri; |
|||
767 | } |
||||
768 | |||||
769 | /** |
||||
770 | * @inheritdoc |
||||
771 | */ |
||||
772 | 1 | public function setTermsOfServiceUri($tosUri) |
|||
773 | { |
||||
774 | 1 | $this->tos_uri = $tosUri; |
|||
775 | 1 | return $this; |
|||
776 | } |
||||
777 | |||||
778 | /** |
||||
779 | * @inheritdoc |
||||
780 | */ |
||||
781 | 1 | public function getContacts() |
|||
782 | { |
||||
783 | 1 | return $this->contacts; |
|||
784 | } |
||||
785 | |||||
786 | /** |
||||
787 | * @inheritdoc |
||||
788 | */ |
||||
789 | 1 | public function setContacts($contacts) |
|||
790 | { |
||||
791 | 1 | $this->contacts = $contacts; |
|||
792 | 1 | return $this; |
|||
793 | } |
||||
794 | |||||
795 | /** |
||||
796 | * @inheritDoc |
||||
797 | */ |
||||
798 | 1 | public function getGrantTypes() |
|||
799 | { |
||||
800 | 1 | return (int)$this->grant_types; |
|||
801 | } |
||||
802 | |||||
803 | /** |
||||
804 | * @inheritDoc |
||||
805 | */ |
||||
806 | 2 | public function setGrantTypes($grantTypes) |
|||
807 | { |
||||
808 | 2 | $grantTypeIds = array_flip(Oauth2Module::GRANT_TYPE_MAPPING); |
|||
809 | 2 | for ($i = (int)log(PHP_INT_MAX, 2); $i >= 0; $i--) { |
|||
810 | 2 | $grantTypeId = (int)pow(2, $i); |
|||
811 | 2 | if ($grantTypes & $grantTypeId) { |
|||
812 | 2 | if (!array_key_exists($grantTypeId, $grantTypeIds)) { |
|||
813 | 1 | throw new InvalidArgumentException('Unknown Grant Type ID: ' . $grantTypeId); |
|||
814 | } |
||||
815 | } |
||||
816 | } |
||||
817 | |||||
818 | 1 | $this->grant_types = $grantTypes; |
|||
819 | |||||
820 | 1 | return $this; |
|||
821 | } |
||||
822 | |||||
823 | /** |
||||
824 | * @inheritDoc |
||||
825 | */ |
||||
826 | 2 | public function validateGrantType($grantTypeIdentifier) |
|||
827 | { |
||||
828 | 2 | $grantTypeId = Oauth2Module::getGrantTypeId($grantTypeIdentifier); |
|||
829 | 2 | if (empty($grantTypeId)) { |
|||
830 | 1 | throw new InvalidArgumentException('Unknown grant type "' . $grantTypeIdentifier . '".'); |
|||
831 | } |
||||
832 | |||||
833 | 1 | return (bool)($this->getGrantTypes() & $grantTypeId); |
|||
834 | } |
||||
835 | |||||
836 | /** |
||||
837 | * @inheritDoc |
||||
838 | * @throws InvalidConfigException |
||||
839 | */ |
||||
840 | 10 | public function validateAuthRequestScopes($scopeIdentifiers, &$unknownScopes = [], &$unauthorizedScopes = []) |
|||
841 | { |
||||
842 | 10 | if (empty($scopeIdentifiers)) { |
|||
843 | 1 | $unknownScopes = []; |
|||
844 | 1 | $unauthorizedScopes = []; |
|||
845 | 1 | return true; |
|||
846 | } |
||||
847 | |||||
848 | /** @var Oauth2ScopeInterface $scopeClass */ |
||||
849 | 9 | $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class); |
|||
850 | 9 | $knownScopeIdentifiers = $scopeClass::find() |
|||
851 | 9 | ->andWhere(['identifier' => $scopeIdentifiers]) |
|||
852 | 9 | ->select('identifier') |
|||
853 | 9 | ->column(); |
|||
854 | |||||
855 | 9 | $unknownScopes = array_diff($scopeIdentifiers, $knownScopeIdentifiers); |
|||
856 | |||||
857 | 9 | $allowedScopeIdentifiers = array_map( |
|||
858 | 9 | fn($scope) => $scope->getIdentifier(), |
|||
859 | 9 | $this->getAllowedScopes($knownScopeIdentifiers) |
|||
860 | 9 | ); |
|||
861 | |||||
862 | 9 | $unauthorizedScopes = array_values(array_diff($knownScopeIdentifiers, $allowedScopeIdentifiers)); |
|||
863 | |||||
864 | 9 | return empty($unknownScopes) && empty($unauthorizedScopes); |
|||
865 | } |
||||
866 | |||||
867 | /** |
||||
868 | * @inheritDoc |
||||
869 | * @throws InvalidConfigException |
||||
870 | */ |
||||
871 | 16 | public function getAllowedScopes($requestedScopeIdentifiers = []) |
|||
872 | { |
||||
873 | /** @var Oauth2ClientScopeInterface $clientScopeClass */ |
||||
874 | 16 | $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class); |
|||
875 | 16 | $clientScopeTableName = $clientScopeClass::tableName(); |
|||
876 | /** @var Oauth2ScopeInterface $scopeClass */ |
||||
877 | 16 | $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class); |
|||
878 | 16 | $scopeTableName = $scopeClass::tableName(); |
|||
879 | |||||
880 | 16 | if (is_array($requestedScopeIdentifiers)) { |
|||
881 | 14 | $possibleScopesConditions = [ |
|||
882 | // Requested and default scopes defined for this client. |
||||
883 | 14 | ['AND', |
|||
884 | 14 | [$clientScopeTableName . '.client_id' => $this->getPrimaryKey()], |
|||
885 | 14 | [$clientScopeTableName . '.enabled' => 1], |
|||
886 | 14 | ['OR', |
|||
887 | 14 | ...( |
|||
888 | 14 | !empty($requestedScopeIdentifiers) |
|||
889 | 9 | ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]] |
|||
890 | : [] |
||||
891 | 14 | ), |
|||
892 | 14 | ['NOT', [ |
|||
893 | 14 | $clientScopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO |
|||
894 | 14 | ]], |
|||
895 | 14 | ['AND', |
|||
896 | 14 | [$clientScopeTableName . '.applied_by_default' => null], |
|||
897 | 14 | ['NOT', [ |
|||
898 | 14 | $scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO |
|||
899 | 14 | ]], |
|||
900 | 14 | ], |
|||
901 | 14 | ], |
|||
902 | 14 | ], |
|||
903 | 14 | ]; |
|||
904 | } else { |
||||
905 | 2 | if ($requestedScopeIdentifiers === true) { |
|||
906 | 2 | $possibleScopesConditions = [ |
|||
907 | // All scopes defined for this client. |
||||
908 | 2 | [$clientScopeTableName . '.enabled' => 1], |
|||
909 | 2 | ]; |
|||
910 | } else { |
||||
911 | throw new InvalidArgumentException('`$possibleScopesConditions` must be either an array of strings or `true`.'); // phpcs:ignore Generic.Files.LineLength.TooLong |
||||
912 | } |
||||
913 | } |
||||
914 | |||||
915 | 16 | $allowGenericScopes = $this->getAllowGenericScopes(); |
|||
916 | 16 | if ($allowGenericScopes) { |
|||
917 | 5 | if (is_array($requestedScopeIdentifiers)) { |
|||
918 | // Requested and default scopes defined by scope for all clients. |
||||
919 | 4 | $possibleScopesConditions[] = ['AND', |
|||
920 | 4 | [$clientScopeTableName . '.client_id' => null], |
|||
921 | 4 | ['OR', |
|||
922 | 4 | ...( |
|||
923 | 4 | !empty($requestedScopeIdentifiers) |
|||
924 | 2 | ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]] |
|||
925 | : [] |
||||
926 | 4 | ), |
|||
927 | 4 | [ |
|||
928 | 4 | 'NOT', |
|||
929 | 4 | [$scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO] |
|||
930 | 4 | ], |
|||
931 | 4 | ], |
|||
932 | 4 | ]; |
|||
933 | 1 | } elseif ($requestedScopeIdentifiers === true) { |
|||
934 | // All scopes defined by scope for all clients. |
||||
935 | 1 | $possibleScopesConditions[] = [$clientScopeTableName . '.client_id' => null]; |
|||
936 | } |
||||
937 | } |
||||
938 | |||||
939 | 16 | return $scopeClass::find() |
|||
940 | 16 | ->joinWith( |
|||
941 | 16 | ['clientScopes' => function (Oauth2ClientScopeQuery $query) use ($clientScopeTableName) { |
|||
942 | 16 | $query->andOnCondition([$clientScopeTableName . '.client_id' => $this->getPrimaryKey()]); |
|||
943 | 16 | }], |
|||
944 | 16 | true |
|||
945 | 16 | ) |
|||
946 | 16 | ->enabled() |
|||
947 | 16 | ->andWhere(['OR', ...$possibleScopesConditions]) |
|||
948 | 16 | ->orderBy('id') |
|||
949 | 16 | ->all(); |
|||
950 | } |
||||
951 | |||||
952 | /** |
||||
953 | * @inheritdoc |
||||
954 | * @return array{ |
||||
0 ignored issues
–
show
|
|||||
955 | * 'unaffected': Oauth2ClientScopeInterface[], |
||||
956 | * 'new': Oauth2ClientScopeInterface[], |
||||
957 | * 'updated': Oauth2ClientScopeInterface[], |
||||
958 | * 'deleted': Oauth2ClientScopeInterface[], |
||||
959 | * } |
||||
960 | */ |
||||
961 | 11 | public function syncClientScopes($scopes, $scopeRepository) |
|||
962 | { |
||||
963 | 11 | if (is_string($scopes)) { |
|||
964 | 2 | $scopes = array_filter(array_map('trim', explode(' ', $scopes))); |
|||
965 | 9 | } elseif ($scopes === null) { |
|||
966 | 1 | $scopes = []; |
|||
967 | 8 | } elseif (!is_array($scopes)) { |
|||
968 | 1 | throw new InvalidArgumentException('$scopes must be a string, an array or null.'); |
|||
969 | } |
||||
970 | |||||
971 | /** @var class-string<Oauth2ClientScopeInterface> $clientScopeClass */ |
||||
972 | 10 | $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class); |
|||
973 | |||||
974 | /** @var Oauth2ClientScopeInterface[] $origClientScopes */ |
||||
975 | 10 | $origClientScopes = $clientScopeClass::findAll([ |
|||
976 | 10 | 'client_id' => $this->getPrimaryKey(), |
|||
977 | 10 | ]); |
|||
978 | |||||
979 | 10 | $origClientScopes = array_combine( |
|||
980 | 10 | array_map( |
|||
981 | 10 | fn(Oauth2ClientScopeInterface $clientScope) => implode('-', $clientScope->getPrimaryKey(true)), |
|||
982 | 10 | $origClientScopes |
|||
983 | 10 | ), |
|||
984 | 10 | $origClientScopes |
|||
985 | 10 | ); |
|||
986 | |||||
987 | /** @var Oauth2ClientScopeInterface[] $clientScopes */ |
||||
988 | 10 | $clientScopes = []; |
|||
989 | |||||
990 | 10 | foreach ($scopes as $key => $value) { |
|||
991 | 9 | if ($value instanceof Oauth2ClientScopeInterface) { |
|||
992 | 2 | $clientScope = $value; |
|||
993 | 2 | $clientScope->client_id = $this->getPrimaryKey(); // Ensure PK is set. |
|||
0 ignored issues
–
show
|
|||||
994 | 2 | $pkIndex = implode('-', $clientScope->getPrimaryKey(true)); |
|||
995 | 2 | if (array_key_exists($pkIndex, $origClientScopes)) { |
|||
996 | // Overwrite orig (might still be considered "unchanged" when new ClientScope is not "dirty"). |
||||
997 | 2 | $origClientScopes[$pkIndex] = $clientScope; |
|||
998 | } |
||||
999 | } else { |
||||
1000 | |||||
1001 | 7 | $scopeIdentifier = null; |
|||
1002 | 7 | $clientScopeConfig = [ |
|||
1003 | 7 | 'client_id' => $this->getPrimaryKey(), |
|||
1004 | 7 | ]; |
|||
1005 | |||||
1006 | 7 | if (is_string($value)) { |
|||
1007 | 3 | $scopeIdentifier = $value; |
|||
1008 | 4 | } elseif ($value instanceof Oauth2ScopeInterface) { |
|||
1009 | 2 | $scopePk = $value->getPrimaryKey(); |
|||
1010 | 2 | if ($scopePk) { |
|||
1011 | 1 | $clientScopeConfig = ArrayHelper::merge( |
|||
1012 | 1 | $clientScopeConfig, |
|||
1013 | 1 | ['scope_id' => $scopePk] |
|||
1014 | 1 | ); |
|||
1015 | } else { |
||||
1016 | // New model, using identifier. |
||||
1017 | 2 | $scopeIdentifier = $value->getIdentifier(); |
|||
1018 | } |
||||
1019 | 2 | } elseif (is_array($value)) { |
|||
1020 | 1 | $clientScopeConfig = ArrayHelper::merge( |
|||
1021 | 1 | $clientScopeConfig, |
|||
1022 | 1 | $value, |
|||
1023 | 1 | ); |
|||
1024 | 1 | if (empty($clientScopeConfig['scope_id'])) { |
|||
1025 | 1 | $scopeIdentifier = $key; |
|||
1026 | } |
||||
1027 | } else { |
||||
1028 | 1 | throw new InvalidArgumentException( |
|||
1029 | 1 | 'If $scopes is an array, its values must be a string, array or an instance of ' |
|||
1030 | 1 | . Oauth2ClientScopeInterface::class . ' or ' . Oauth2ScopeInterface::class . '.' |
|||
1031 | 1 | ); |
|||
1032 | } |
||||
1033 | |||||
1034 | 6 | if (isset($scopeIdentifier)) { |
|||
1035 | 4 | $scope = $scopeRepository->getScopeEntityByIdentifier($scopeIdentifier); |
|||
1036 | 4 | if (empty($scope)) { |
|||
1037 | 1 | throw new InvalidArgumentException('No scope with identifier "' |
|||
1038 | 1 | . $scopeIdentifier . '" found.'); |
|||
1039 | } |
||||
1040 | 3 | if (!($scope instanceof Oauth2ScopeInterface)) { |
|||
1041 | throw new InvalidConfigException(get_class($scope) |
||||
1042 | . ' must implement ' . Oauth2ScopeInterface::class); |
||||
1043 | } |
||||
1044 | 3 | $clientScopeConfig['scope_id'] = $scope->getPrimaryKey(); |
|||
1045 | } else { |
||||
1046 | 3 | if (empty($clientScopeConfig['scope_id'])) { |
|||
1047 | 1 | throw new InvalidArgumentException('Element ' . $key |
|||
1048 | 1 | . ' in $scope should specify either the scope id or its identifier.'); |
|||
1049 | } |
||||
1050 | } |
||||
1051 | |||||
1052 | 4 | $pkIndex = $clientScopeConfig['client_id'] . '-' . $clientScopeConfig['scope_id']; |
|||
1053 | 4 | if (array_key_exists($pkIndex, $origClientScopes)) { |
|||
1054 | 4 | $clientScope = $origClientScopes[$pkIndex]; |
|||
1055 | 4 | $clientScope->setAttributes($clientScopeConfig, false); |
|||
1056 | } else { |
||||
1057 | /** @var Oauth2ClientScopeInterface $clientScope */ |
||||
1058 | 4 | $clientScope = Yii::createObject(ArrayHelper::merge( |
|||
1059 | 4 | ['class' => $clientScopeClass], |
|||
1060 | 4 | $clientScopeConfig |
|||
1061 | 4 | )); |
|||
1062 | } |
||||
1063 | } |
||||
1064 | |||||
1065 | 6 | $pkIndex = implode('-', $clientScope->getPrimaryKey(true)); |
|||
1066 | 6 | $clientScopes[$pkIndex] = $clientScope; |
|||
1067 | } |
||||
1068 | |||||
1069 | 7 | $transaction = static::getDb()->beginTransaction(); |
|||
1070 | try { |
||||
1071 | // Delete records no longer present in the provided data. |
||||
1072 | /** @var self[]|array[] $deleteClientScopes */ |
||||
1073 | 7 | $deleteClientScopes = array_diff_key($origClientScopes, $clientScopes); |
|||
1074 | 7 | foreach ($deleteClientScopes as $deleteClientScope) { |
|||
1075 | 6 | $deleteClientScope->delete(); |
|||
1076 | } |
||||
1077 | |||||
1078 | // Create records not present in the provided data. |
||||
1079 | 7 | $createClientScopes = array_diff_key($clientScopes, $origClientScopes); |
|||
1080 | 7 | foreach ($createClientScopes as $createClientScope) { |
|||
1081 | 6 | $createClientScope->persist(); |
|||
1082 | } |
||||
1083 | |||||
1084 | // Update existing records if needed. |
||||
1085 | 6 | $unaffectedClientScopes = []; |
|||
1086 | 6 | $updatedClientScopes = []; |
|||
1087 | 6 | foreach (array_intersect_key($origClientScopes, $clientScopes) as $key => $existingClientScope) { |
|||
1088 | 5 | if ($existingClientScope->getDirtyAttributes()) { |
|||
1089 | 2 | $existingClientScope->persist(); |
|||
1090 | 2 | $updatedClientScopes[$key] = $existingClientScope; |
|||
1091 | } else { |
||||
1092 | 5 | $unaffectedClientScopes[$key] = $existingClientScope; |
|||
1093 | } |
||||
1094 | } |
||||
1095 | |||||
1096 | 6 | $transaction->commit(); |
|||
1097 | 1 | } catch (\Exception $e) { |
|||
1098 | 1 | $transaction->rollBack(); |
|||
1099 | 1 | throw $e; |
|||
1100 | } |
||||
1101 | |||||
1102 | 6 | return [ |
|||
1103 | 6 | 'unaffected' => $unaffectedClientScopes, |
|||
1104 | 6 | 'new' => $createClientScopes, |
|||
1105 | 6 | 'updated' => $updatedClientScopes, |
|||
1106 | 6 | 'deleted' => $deleteClientScopes, |
|||
1107 | 6 | ]; |
|||
1108 | } |
||||
1109 | } |
||||
1110 |