diff --git a/config-templates/module_oidc.php b/config-templates/module_oidc.php index ad985a17..30064f74 100644 --- a/config-templates/module_oidc.php +++ b/config-templates/module_oidc.php @@ -368,6 +368,33 @@ // 'eyJ...GHg', ], + // (optional) Federation participation limit by Trust Marks. This is an array with the following format: + // [ + // 'trust-anchor-id' => [ + // 'limit-id' => [ + // 'trust-mark-id', + // 'trust-mark-id-2', + // ], + // ], + // ], + // Check example below on how this can be used. If federation participation limit is configured for particular + // Trust Anchor ID, at least one combination of "limit ID" => "trust mark list" should be defined. + ModuleConfig::OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS => [ + // We are limiting federation participation using Trust Marks for 'https://ta.example.org/'. + 'https://ta.example.org/' => [ + // Entities must have (at least) one Trust Mark from the list below. + \SimpleSAML\Module\oidc\Codebooks\LimitsEnum::OneOf->value => [ + 'trust-mark-id', + 'trust-mark-id-2', + ], + // Entities must have all Trust Marks from the list below. + \SimpleSAML\Module\oidc\Codebooks\LimitsEnum::AllOf->value => [ + 'trust-mark-id-3', + 'trust-mark-id-4', + ], + ], + ], + // (optional) Dedicated federation cache adapter, used to cache federation artifacts like trust chains, entity // statements, etc. It will also be used for token reuse check in federation context. Setting this option is // recommended in production environments. If set to null, no caching will be used. Can be set to any diff --git a/routing/routes/routes.php b/routing/routes/routes.php index 12f96743..6d52f78a 100644 --- a/routing/routes/routes.php +++ b/routing/routes/routes.php @@ -10,6 +10,7 @@ use SimpleSAML\Module\oidc\Controllers\AccessTokenController; use SimpleSAML\Module\oidc\Controllers\Admin\ClientController; use SimpleSAML\Module\oidc\Controllers\Admin\ConfigController; +use SimpleSAML\Module\oidc\Controllers\Admin\TestController; use SimpleSAML\Module\oidc\Controllers\AuthorizationController; use SimpleSAML\Module\oidc\Controllers\ConfigurationDiscoveryController; use SimpleSAML\Module\oidc\Controllers\EndSessionController; @@ -57,6 +58,12 @@ ->controller([ClientController::class, 'delete']) ->methods([HttpMethodsEnum::POST->value]); + // Testing + + $routes->add(RoutesEnum::AdminTestTrustChainResolution->name, RoutesEnum::AdminTestTrustChainResolution->value) + ->controller([TestController::class, 'trustChainResolution']) + ->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]); + /***************************************************************************************************************** * OpenID Connect ****************************************************************************************************************/ diff --git a/routing/services/services.yml b/routing/services/services.yml index 75e6030e..f120f146 100644 --- a/routing/services/services.yml +++ b/routing/services/services.yml @@ -99,6 +99,8 @@ services: factory: ['@SimpleSAML\Module\oidc\Factories\ResourceServerFactory', 'build'] # Utils + SimpleSAML\Module\oidc\Utils\Debug\ArrayLogger: ~ + SimpleSAML\Module\oidc\Utils\FederationParticipationValidator: ~ SimpleSAML\Module\oidc\Utils\Routes: ~ SimpleSAML\Module\oidc\Utils\RequestParamsResolver: ~ SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~ diff --git a/src/Codebooks/LimitsEnum.php b/src/Codebooks/LimitsEnum.php new file mode 100644 index 00000000..90dd4993 --- /dev/null +++ b/src/Codebooks/LimitsEnum.php @@ -0,0 +1,11 @@ +authorization->requireAdmin(true); + } + + /** + * @throws \SimpleSAML\Error\ConfigurationError + * @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException + * @throws \SimpleSAML\Module\oidc\Exceptions\OidcException + */ + public function trustChainResolution(Request $request): Response + { + $this->arrayLogger->setWeight(ArrayLogger::WEIGHT_WARNING); + // Let's create new Federation instance so we can inject our debug logger and go without cache. + $federation = new Federation( + supportedAlgorithms: $this->federation->supportedAlgorithms(), + cache: null, + logger: $this->arrayLogger, + ); + + $leafEntityId = $this->moduleConfig->getIssuer(); + $trustChainBag = null; + $resolvedMetadata = []; + $isFormSubmitted = false; + + try { + $trustAnchorIds = $this->moduleConfig->getFederationTrustAnchorIds(); + } catch (\Throwable $exception) { + $this->arrayLogger->error('Module config error: ' . $exception->getMessage()); + $trustAnchorIds = []; + } + + if ($request->isMethod(Request::METHOD_POST)) { + $isFormSubmitted = true; + + !empty($leafEntityId = $request->request->getString('leafEntityId')) || + throw new OidcException('Empty leaf entity ID.'); + !empty($rawTrustAnchorIds = $request->request->getString('trustAnchorIds')) || + throw new OidcException('Empty Trust Anchor IDs.'); + + /** @var non-empty-array $trustAnchorIds */ + $trustAnchorIds = $this->helpers->str()->convertTextToArray($rawTrustAnchorIds); + + try { + $trustChainBag = $federation->trustChainResolver()->for($leafEntityId, $trustAnchorIds); + + foreach ($trustChainBag->getAll() as $index => $trustChain) { + $metadataEntries = []; + foreach (EntityTypesEnum::cases() as $entityTypeEnum) { + try { + $metadataEntries[$entityTypeEnum->value] = + $trustChain->getResolvedMetadata($entityTypeEnum); + } catch (\Throwable $exception) { + $this->arrayLogger->error( + 'Metadata resolving error: ' . $exception->getMessage(), + compact('index', 'entityTypeEnum'), + ); + continue; + } + } + $resolvedMetadata[$index] = array_filter($metadataEntries); + } + } catch (TrustChainException $exception) { + $this->arrayLogger->error('Trust chain error: ' . $exception->getMessage()); + } + } + + $trustAnchorIds = implode("\n", $trustAnchorIds); + $logMessages = $this->arrayLogger->getEntries(); +//dd($this->arrayLogger->getEntries()); + return $this->templateFactory->build( + 'oidc:tests/trust-chain-resolution.twig', + compact( + 'leafEntityId', + 'trustAnchorIds', + 'trustChainBag', + 'resolvedMetadata', + 'logMessages', + 'isFormSubmitted', + ), + RoutesEnum::AdminTestTrustChainResolution->value, + ); + } +} diff --git a/src/Controllers/Federation/Test.php b/src/Controllers/Federation/Test.php index f0e06be4..97eaaee4 100644 --- a/src/Controllers/Federation/Test.php +++ b/src/Controllers/Federation/Test.php @@ -65,25 +65,25 @@ public function __invoke(): Response // $requestObject = $requestObjectFactory->fromToken($unprotectedJws); // dd($requestObject, $requestObject->getPayload(), $requestObject->getHeader()); -// $cache->clear(); + $this->federationCache?->cache->clear(); $trustChain = $this->federation ->trustChainResolver() ->for( - 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/', +// 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/', // 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/oidc/rp/', // 'https://relying-party-php.testbed.oidcfed.incubator.geant.org/', -// 'https://gorp.testbed.oidcfed.incubator.geant.org', + 'https://gorp.testbed.oidcfed.incubator.geant.org', // 'https://maiv1.incubator.geant.org', [ -// 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/', + 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/', 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ABTrustAnchor/', -// 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/CTrustAnchor/', + 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/CTrustAnchor/', ], - ); - + )->getAll(); +dd($trustChain); $leaf = $trustChain->getResolvedLeaf(); -// dd($leaf); + dd($leaf->getPayload()); $leafFederationJwks = $leaf->getJwks(); // dd($leafFederationJwks); // /** @psalm-suppress PossiblyNullArgument */ diff --git a/src/Factories/RequestRulesManagerFactory.php b/src/Factories/RequestRulesManagerFactory.php index ffc2ec4f..8aa57b32 100644 --- a/src/Factories/RequestRulesManagerFactory.php +++ b/src/Factories/RequestRulesManagerFactory.php @@ -36,6 +36,7 @@ use SimpleSAML\Module\oidc\Services\LoggerService; use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor; use SimpleSAML\Module\oidc\Utils\FederationCache; +use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator; use SimpleSAML\Module\oidc\Utils\JwksResolver; use SimpleSAML\Module\oidc\Utils\ProtocolCache; use SimpleSAML\Module\oidc\Utils\RequestParamsResolver; @@ -58,6 +59,7 @@ public function __construct( private readonly Federation $federation, private readonly Helpers $helpers, private readonly JwksResolver $jwksResolver, + private readonly FederationParticipationValidator $federationParticipationValidator, private readonly ?FederationCache $federationCache = null, private readonly ?ProtocolCache $protocolCache = null, ) { @@ -88,6 +90,7 @@ private function getDefaultRules(): array $this->federation, $this->helpers, $this->jwksResolver, + $this->federationParticipationValidator, $this->federationCache, ), new RedirectUriRule($this->requestParamsResolver), diff --git a/src/Factories/TemplateFactory.php b/src/Factories/TemplateFactory.php index a3039779..0350af95 100644 --- a/src/Factories/TemplateFactory.php +++ b/src/Factories/TemplateFactory.php @@ -107,6 +107,13 @@ protected function includeDefaultMenuItems(): void ), ); + $this->oidcMenu->addItem( + $this->oidcMenu->buildItem( + $this->moduleConfig->getModuleUrl(RoutesEnum::AdminClients->value), + Translate::noop('Client Registry'), + ), + ); + $this->oidcMenu->addItem( $this->oidcMenu->buildItem( $this->moduleConfig->getModuleUrl(RoutesEnum::AdminConfigProtocol->value), @@ -123,8 +130,8 @@ protected function includeDefaultMenuItems(): void $this->oidcMenu->addItem( $this->oidcMenu->buildItem( - $this->moduleConfig->getModuleUrl(RoutesEnum::AdminClients->value), - Translate::noop('Client Registry'), + $this->moduleConfig->getModuleUrl(RoutesEnum::AdminTestTrustChainResolution->value), + Translate::noop('Test Trust Chain Resolution'), ), ); } diff --git a/src/Forms/ClientForm.php b/src/Forms/ClientForm.php index 3285f028..c0a272d3 100644 --- a/src/Forms/ClientForm.php +++ b/src/Forms/ClientForm.php @@ -414,6 +414,7 @@ protected function getScopes(): array } /** + * TODO mivanci Move to Str helper. * @return string[] */ protected function convertTextToArrayWithLinesAsValues(string $text): array diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index e15185e5..c7df69ac 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -14,4 +14,25 @@ public function ensureStringValues(array $values): array { return array_map(fn(mixed $value): string => (string)$value, $values); } + + public function isValueOneOf(mixed $value, array $set): bool + { + $value = is_array($value) ? $value : [$value]; + return !empty(array_intersect($value, $set)); + } + + public function isValueSubsetOf(mixed $value, array $superset): bool + { + $value = is_array($value) ? $value : [$value]; + + return empty(array_diff($value, $superset)); + } + + public function isValueSupersetOf(mixed $value, array $subset): bool + { + $value = is_array($value) ? $value : [$value]; + + // Opposite of subset... + return $this->isValueSubsetOf($subset, $value); + } } diff --git a/src/Helpers/Str.php b/src/Helpers/Str.php index 4674c786..9218119d 100644 --- a/src/Helpers/Str.php +++ b/src/Helpers/Str.php @@ -16,4 +16,16 @@ public function convertScopesStringToArray(string $scopes, string $delimiter = ' { return array_filter(explode($delimiter, trim($scopes)), fn($scope) => !empty($scope)); } + + /** + * @param non-empty-string $pattern + * @return string[] + */ + public function convertTextToArray(string $text, string $pattern = "/[\t\r\n]+/"): array + { + return array_filter( + preg_split($pattern, $text), + fn(string $line): bool => !empty(trim($line)), + ); + } } diff --git a/src/ModuleConfig.php b/src/ModuleConfig.php index 973d1f16..0ed106de 100644 --- a/src/ModuleConfig.php +++ b/src/ModuleConfig.php @@ -81,6 +81,8 @@ class ModuleConfig final public const OPTION_PROTOCOL_CACHE_ADAPTER = 'protocol_cache_adapter'; final public const OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS = 'protocol_cache_adapter_arguments'; final public const OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION = 'protocol_user_entity_cache_duration'; + final public const OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS = + 'federation_participation_limit_by_trust_marks'; protected static array $standardScopes = [ ScopesEnum::OpenId->value => [ @@ -465,7 +467,7 @@ public function getProtocolUserEntityCacheDuration(): DateInterval /***************************************************************************************************************** - * OpenID Connect related config. + * OpenID Federation related config. ****************************************************************************************************************/ public function getFederationEnabled(): bool @@ -669,4 +671,33 @@ public function getTrustAnchorJwks(string $trustAnchorId): ?array sprintf('Unexpected JWKS format for Trust Anchor %s: %s', $trustAnchorId, var_export($jwks, true)), ); } + + public function getFederationParticipationLimitByTrustMarks(): array + { + return $this->config()->getOptionalArray( + self::OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS, + [], + ); + } + + /** + * @throws \SimpleSAML\Error\ConfigurationError + */ + public function getTrustMarksNeededForFederationParticipationFor(string $trustAnchorId): array + { + $participationLimit = $this->getFederationParticipationLimitByTrustMarks()[$trustAnchorId] ?? []; + if (!is_array($participationLimit)) { + throw new ConfigurationError('Invalid configuration for federation participation limit.'); + } + + return $participationLimit; + } + + /** + * @throws \SimpleSAML\Error\ConfigurationError + */ + public function isFederationParticipationLimitedByTrustMarksFor(string $trustAnchorId): bool + { + return !empty($this->getTrustMarksNeededForFederationParticipationFor($trustAnchorId)); + } } diff --git a/src/Server/RequestRules/Rules/ClientIdRule.php b/src/Server/RequestRules/Rules/ClientIdRule.php index 7f64bb61..bd66acec 100644 --- a/src/Server/RequestRules/Rules/ClientIdRule.php +++ b/src/Server/RequestRules/Rules/ClientIdRule.php @@ -19,6 +19,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Result; use SimpleSAML\Module\oidc\Services\LoggerService; use SimpleSAML\Module\oidc\Utils\FederationCache; +use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator; use SimpleSAML\Module\oidc\Utils\JwksResolver; use SimpleSAML\Module\oidc\Utils\RequestParamsResolver; use SimpleSAML\OpenID\Codebooks\EntityTypesEnum; @@ -39,6 +40,7 @@ public function __construct( protected Federation $federation, protected Helpers $helpers, protected JwksResolver $jwksResolver, + protected FederationParticipationValidator $federationParticipationValidator, protected ?FederationCache $federationCache = null, ) { parent::__construct($requestParamsResolver); @@ -125,7 +127,7 @@ public function checkRule( $trustChain = $this->federation->trustChainResolver()->for( $clientEntityId, $this->moduleConfig->getFederationTrustAnchorIds(), - ); + )->getShortest(); } catch (ConfigurationError $exception) { throw OidcServerException::serverError( 'invalid OIDC configuration: ' . $exception->getMessage(), @@ -191,7 +193,16 @@ public function checkRule( // Verify signature on Request Object using client JWKS. $requestObject->verifyWithKeySet($clientJwks); - // Signature verified, we can persist (new) client registration. + // Check if federation participation is limited by Trust Marks. + if ( + $this->moduleConfig->isFederationParticipationLimitedByTrustMarksFor( + $trustChain->getResolvedTrustAnchor()->getIssuer(), + ) + ) { + $this->federationParticipationValidator->byTrustMarksFor($trustChain); + } + + // All is verified, We can persist (new) client registration. if ($existingClient) { $this->clientRepository->update($registrationClient); } else { diff --git a/src/Services/Container.php b/src/Services/Container.php index e9a3faa1..5a4a46cb 100644 --- a/src/Services/Container.php +++ b/src/Services/Container.php @@ -103,6 +103,7 @@ use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor; use SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder; use SimpleSAML\Module\oidc\Utils\FederationCache; +use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator; use SimpleSAML\Module\oidc\Utils\JwksResolver; use SimpleSAML\Module\oidc\Utils\ProtocolCache; use SimpleSAML\Module\oidc\Utils\RequestParamsResolver; @@ -346,6 +347,11 @@ public function __construct() $jwksResolver = new JwksResolver($jwks); $this->services[JwksResolver::class] = $jwksResolver; + $federationParticipationValidator = new FederationParticipationValidator( + $moduleConfig, + $loggerService, + ); + $this->services[FederationParticipationValidator::class] = $federationParticipationValidator; $requestRules = [ new StateRule($requestParamsResolver), @@ -357,6 +363,7 @@ public function __construct() $federation, $helpers, $jwksResolver, + $federationParticipationValidator, $federationCache, ), new RedirectUriRule($requestParamsResolver), diff --git a/src/Utils/Debug/ArrayLogger.php b/src/Utils/Debug/ArrayLogger.php new file mode 100644 index 00000000..d228693f --- /dev/null +++ b/src/Utils/Debug/ArrayLogger.php @@ -0,0 +1,160 @@ +setWeight($weight); + } + + public function setWeight(int $weight): void + { + $this->weight = max(self::WEIGHT_DEBUG, min($weight, self::WEIGHT_EMERGENCY)); + } + + /** + * @inheritDoc + */ + public function emergency(\Stringable|string $message, array $context = []): void + { + // Always log emergency. + $this->entries[] = $this->prepareEntry(LogLevel::EMERGENCY, $message, $context); + } + + /** + * @inheritDoc + */ + public function alert(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGH_ALERT) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::ALERT, $message, $context); + } + + /** + * @inheritDoc + */ + public function critical(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGHT_CRITICAL) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::CRITICAL, $message, $context); + } + + /** + * @inheritDoc + */ + public function error(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGHT_ERROR) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::ERROR, $message, $context); + } + + /** + * @inheritDoc + */ + public function warning(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGHT_WARNING) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::WARNING, $message, $context); + } + + /** + * @inheritDoc + */ + public function notice(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGHT_NOTICE) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::NOTICE, $message, $context); + } + + /** + * @inheritDoc + */ + public function info(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGHT_INFO) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::INFO, $message, $context); + } + + /** + * @inheritDoc + */ + public function debug(\Stringable|string $message, array $context = []): void + { + if ($this->weight > self::WEIGHT_DEBUG) { + return; + } + $this->entries[] = $this->prepareEntry(LogLevel::DEBUG, $message, $context); + } + + /** + * @inheritDoc + */ + public function log($level, \Stringable|string $message, array $context = []): void + { + match ($level) { + LogLevel::EMERGENCY => $this->emergency($message, $context), + LogLevel::ALERT => $this->alert($message, $context), + LogLevel::CRITICAL => $this->critical($message, $context), + LogLevel::ERROR => $this->error($message, $context), + LogLevel::WARNING => $this->warning($message, $context), + LogLevel::NOTICE => $this->notice($message, $context), + LogLevel::INFO => $this->info($message, $context), + LogLevel::DEBUG => $this->debug($message, $context), + default => throw new InvalidArgumentException("Unrecognized log level '$level''"), + }; + } + + public function getEntries(): array + { + return $this->entries; + } + + protected function prepareEntry(string $logLevel, \Stringable|string $message, array $context = []): string + { + return sprintf( + '%s %s %s %s', + $this->helpers->dateTime()->getUtc()->format(DateTimeInterface::RFC3339_EXTENDED), + strtoupper($logLevel), + $message, + empty($context) ? '' : 'Context: ' . var_export($context, true), + ); + } +} diff --git a/src/Utils/FederationParticipationValidator.php b/src/Utils/FederationParticipationValidator.php new file mode 100644 index 00000000..06bd37a5 --- /dev/null +++ b/src/Utils/FederationParticipationValidator.php @@ -0,0 +1,38 @@ +getResolvedTrustAnchor(); + + $trustMarkLimitsRules = $this->moduleConfig + ->getTrustMarksNeededForFederationParticipationFor($trustAnchor->getIssuer()); + + if (empty($trustMarkLimitsRules)) { + $this->loggerService->debug('No Trust Mark limits emposed for ' . $trustAnchor->getIssuer()); + return; + } + + $this->loggerService->debug('Trust Mark limits for ' . $trustAnchor->getIssuer(), $trustMarkLimitsRules); + + //$leaf = $trustChain->getResolvedLeaf(); + //$leafTrustMarks = $leaf->getTrustMarks(); + + // TODO mivanci continue + } +} diff --git a/src/Utils/Routes.php b/src/Utils/Routes.php index d256adf9..7b87f514 100644 --- a/src/Utils/Routes.php +++ b/src/Utils/Routes.php @@ -134,6 +134,13 @@ public function urlAdminClientsDelete(string $clientId, array $parameters = []): return $this->getModuleUrl(RoutesEnum::AdminClientsDelete->value, $parameters); } + // Testing + + public function urlAdminTestTrustChainResolution(array $parameters = []): string + { + return $this->getModuleUrl(RoutesEnum::AdminTestTrustChainResolution->value, $parameters); + } + /***************************************************************************************************************** * OpenID Connect URLs. ****************************************************************************************************************/ diff --git a/templates/tests/trust-chain-resolution.twig b/templates/tests/trust-chain-resolution.twig new file mode 100644 index 00000000..972aa720 --- /dev/null +++ b/templates/tests/trust-chain-resolution.twig @@ -0,0 +1,91 @@ +{% set subPageTitle = 'Test Trust Chain Resolution'|trans %} + +{% extends "@oidc/base.twig" %} + +{% block oidcContent %} + +

+ {{ 'You can use the form below to test Trust Chain resolution from a leaf entity ID to Trust Anchors.'|trans }} + {{ 'By default, form is populated with current OP issuer and configured Trust Anchors, but you are free to adjust entries as needed.'|trans }} + {{ 'Log messages will show if any warnings or errors were raised during chain resolution.'|trans }} +

+ +
+ +
+ + + + + + + {{ 'Enter one Trust Anchor ID per line.'|trans }} + +
+ +
+
+ + {% if isFormSubmitted|default %} + +

{{ 'Log messages'|trans }}

+

+ {% if logMessages|default %} + + {{- logMessages|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_SLASHES')) -}} + + {% else %} + {{ 'No entries.'|trans }} + {% endif %} +

+ +

{{ 'Resolved chains'|trans }}

+ {% if trustChainBag|default %} +

+ {{ 'Total chains:'|trans }} {{ trustChainBag.getCount }} +

+ {% for index, trustChain in trustChainBag.getAll %} +

+ {{ loop.index }}. {{ 'Trust Anchor ID:'|trans }} {{ trustChain.getResolvedTrustAnchor.getIssuer }} +

+ {{ 'Path:'|trans }} +
+ {% for entity in trustChain.getEntities %} + {% if loop.index > 1 %} + ⇘ {{ loop.index0 }}. {{ entity.getSubject }}
+ {% endif %} + {% endfor %} + +
+ {{ 'Resolved metadata:' }}
+ {% if resolvedMetadata[index]|default is not empty %} + + {{- resolvedMetadata[index]|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_SLASHES')) -}} + + {% else %} + {{ 'N/A'|trans }} + {% endif %} +

+ {% if not loop.last %} +

+ {% endif %} + {% endfor %} + {% else %} +

{{ 'No entries.'|trans }}

+ {% endif %} + + {% endif %} + +{% endblock oidcContent -%} diff --git a/tests/unit/src/Helpers/ArrTest.php b/tests/unit/src/Helpers/ArrTest.php new file mode 100644 index 00000000..a6fdd7e1 --- /dev/null +++ b/tests/unit/src/Helpers/ArrTest.php @@ -0,0 +1,49 @@ +assertTrue($this->sut()->isValueOneOf('a', ['a'])); + $this->assertTrue($this->sut()->isValueOneOf(['a'], ['a'])); + $this->assertTrue($this->sut()->isValueOneOf(['a', 'b'], ['a'])); + + $this->assertFalse($this->sut()->isValueOneOf('a', ['b'])); + $this->assertFalse($this->sut()->isValueOneOf(['a'], ['b'])); + } + + public function testIsValueSubsetOf(): void + { + $this->assertTrue($this->sut()->isValueSubsetOf('a', ['a', 'b', 'c'])); + $this->assertTrue($this->sut()->isValueSubsetOf(['a'], ['a', 'b', 'c'])); + $this->assertTrue($this->sut()->isValueSubsetOf(['a', 'b'], ['a', 'b', 'c'])); + + $this->assertFalse($this->sut()->isValueSubsetOf('a', [])); + $this->assertFalse($this->sut()->isValueSubsetOf('a', ['b'])); + $this->assertFalse($this->sut()->isValueSubsetOf(['a', 'c'], ['b'])); + } + + public function testIsValueSupersetOf(): void + { + $this->assertTrue($this->sut()->isValueSupersetOf('a', ['a'])); + $this->assertTrue($this->sut()->isValueSupersetOf(['a'], ['a'])); + $this->assertTrue($this->sut()->isValueSupersetOf(['a', 'b'], ['a'])); + + $this->assertFalse($this->sut()->isValueSupersetOf('a', ['b'])); + $this->assertFalse($this->sut()->isValueSupersetOf(['a'], ['b'])); + } +} diff --git a/tests/unit/src/Server/RequestRules/Rules/ClientIdRuleTest.php b/tests/unit/src/Server/RequestRules/Rules/ClientIdRuleTest.php index 5bf55763..fda3d8ae 100644 --- a/tests/unit/src/Server/RequestRules/Rules/ClientIdRuleTest.php +++ b/tests/unit/src/Server/RequestRules/Rules/ClientIdRuleTest.php @@ -18,6 +18,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ClientIdRule; use SimpleSAML\Module\oidc\Services\LoggerService; use SimpleSAML\Module\oidc\Utils\FederationCache; +use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator; use SimpleSAML\Module\oidc\Utils\JwksResolver; use SimpleSAML\Module\oidc\Utils\RequestParamsResolver; use SimpleSAML\OpenID\Federation; @@ -39,6 +40,7 @@ class ClientIdRuleTest extends TestCase protected Stub $clientEntityFactoryStub; protected Stub $helpersStub; protected Stub $jwksResolverStub; + protected Stub $federationParticipationValidatorStub; /** * @throws \Exception @@ -57,9 +59,10 @@ protected function setUp(): void $this->clientEntityFactoryStub = $this->createStub(ClientEntityFactory::class); $this->helpersStub = $this->createStub(Helpers::class); $this->jwksResolverStub = $this->createStub(JwksResolver::class); + $this->federationParticipationValidatorStub = $this->createStub(FederationParticipationValidator::class); } - protected function mock(): ClientIdRule + protected function sut(): ClientIdRule { return new ClientIdRule( $this->requestParamsResolverStub, @@ -69,20 +72,21 @@ protected function mock(): ClientIdRule $this->federationStub, $this->helpersStub, $this->jwksResolverStub, + $this->federationParticipationValidatorStub, $this->federationCacheStub, ); } public function testConstruct(): void { - $this->assertInstanceOf(ClientIdRule::class, $this->mock()); + $this->assertInstanceOf(ClientIdRule::class, $this->sut()); } public function testCheckRuleEmptyClientIdThrows(): void { $this->requestParamsResolverStub->method('getBasedOnAllowedMethods')->willReturn(null); $this->expectException(OidcServerException::class); - $this->mock()->checkRule( + $this->sut()->checkRule( $this->requestStub, $this->resultBagStub, $this->loggerServiceStub, @@ -94,7 +98,7 @@ public function testCheckRuleInvalidClientThrows(): void $this->requestParamsResolverStub->method('getBasedOnAllowedMethods')->willReturn('123'); $this->clientRepositoryStub->method('getClientEntity')->willReturn('invalid'); $this->expectException(OidcServerException::class); - $this->mock()->checkRule( + $this->sut()->checkRule( $this->requestStub, $this->resultBagStub, $this->loggerServiceStub, @@ -110,7 +114,7 @@ public function testCheckRuleForValidClientId(): void $this->requestParamsResolverStub->method('getBasedOnAllowedMethods')->willReturn('123'); $this->clientRepositoryStub->method('getClientEntity')->willReturn($this->clientEntityStub); - $result = $this->mock()->checkRule( + $result = $this->sut()->checkRule( $this->requestStub, $this->resultBagStub, $this->loggerServiceStub, diff --git a/tests/unit/src/Utils/Debug/ArrayLoggerTest.php b/tests/unit/src/Utils/Debug/ArrayLoggerTest.php new file mode 100644 index 00000000..bd519163 --- /dev/null +++ b/tests/unit/src/Utils/Debug/ArrayLoggerTest.php @@ -0,0 +1,89 @@ +helpersMock = $this->createMock(Helpers::class); + $this->dateTimeMock = $this->createMock(Helpers\DateTime::class); + $this->helpersMock->method('dateTime')->willReturn($this->dateTimeMock); + $this->dateTimeMock->method('getUtc')->willReturn(new \DateTimeImmutable()); + $this->weight = ArrayLogger::WEIGHT_DEBUG; + } + + protected function sut( + ?Helpers $helpers = null, + ?int $weight = null, + ): ArrayLogger { + $helpers ??= $this->helpersMock; + $weight ??= $this->weight; + + return new ArrayLogger($helpers, $weight); + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(ArrayLogger::class, $this->sut()); + } + + public function testCanLogEntriesBasedOnWeight(): void + { + $sut = $this->sut(); + $this->assertEmpty($sut->getEntries()); + + $sut->debug('debug message'); + $sut->info('info message'); + $sut->notice('notice message'); + $sut->warning('warning message'); + $sut->error('error message'); + $sut->critical('critical message'); + $sut->alert('alert message'); + $sut->emergency('emergency message'); + $sut->log(LogLevel::DEBUG, 'debug message'); + + $this->assertCount(9, $sut->getEntries()); + + } + + public function testWontLogLessThanEmergency(): void + { + $sut = $this->sut(weight: ArrayLogger::WEIGHT_EMERGENCY); + + $sut->debug('debug message'); + $sut->info('info message'); + $sut->notice('notice message'); + $sut->warning('warning message'); + $sut->error('error message'); + $sut->critical('critical message'); + $sut->alert('alert message'); + + $this->assertEmpty($sut->getEntries()); + + $sut->emergency('emergency message'); + $this->assertNotEmpty($sut->getEntries()); + } + + public function testThrowsOnInvalidLogLevel(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->sut()->log('invalid', 'message'); + } +}