From e68d80eb4b4d6d25a3f6a8cd0f60076a2c1318bb Mon Sep 17 00:00:00 2001 From: Marko Ivancic <marko.ivancic@srce.hr> Date: Tue, 11 Jul 2023 09:49:55 +0000 Subject: [PATCH] Show service logo if available --- composer.lock | 65 +++++---- public/assets/css/src/icons/no-image.svg | 2 + src/Entities/Bases/AbstractProvider.php | 34 +++-- src/Entities/Interfaces/ProviderInterface.php | 7 +- .../Providers/Bases/AbstractOidcProvider.php | 34 +++++ .../Providers/Bases/AbstractSaml2Provider.php | 95 ++++++++++++ src/Entities/Providers/Identity/Oidc.php | 12 +- src/Entities/Providers/Identity/Saml2.php | 37 +---- src/Entities/Providers/Service/Oidc.php | 12 +- src/Entities/Providers/Service/Saml2.php | 40 +----- src/Helpers/Arr.php | 29 ++++ templates/user/connected-organizations.twig | 9 +- tests/src/Constants/StateArrays.php | 33 ++++- .../Entities/Bases/AbstractProviderTest.php | 10 ++ .../Bases/AbstractOidcProviderTest.php | 81 +++++++++++ .../Bases/AbstractSaml2ProviderTest.php | 136 ++++++++++++++++++ .../Entities/Providers/Identity/OidcTest.php | 2 + .../Entities/Providers/Identity/Saml2Test.php | 2 + .../Entities/Providers/Service/OidcTest.php | 2 + .../Entities/Providers/Service/Saml2Test.php | 2 + tests/src/Helpers/ArrayTest.php | 33 +++++ tests/src/Helpers/ProviderResolverTest.php | 4 + 22 files changed, 552 insertions(+), 129 deletions(-) create mode 100644 public/assets/css/src/icons/no-image.svg create mode 100644 src/Entities/Providers/Bases/AbstractOidcProvider.php create mode 100644 src/Entities/Providers/Bases/AbstractSaml2Provider.php create mode 100644 tests/src/Entities/Providers/Bases/AbstractOidcProviderTest.php create mode 100644 tests/src/Entities/Providers/Bases/AbstractSaml2ProviderTest.php diff --git a/composer.lock b/composer.lock index 392ace6..8696b4c 100644 --- a/composer.lock +++ b/composer.lock @@ -154,16 +154,16 @@ }, { "name": "doctrine/dbal", - "version": "3.6.2", + "version": "3.6.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c" + "reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b4bd1cfbd2b916951696d82e57d054394d84864c", - "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/9a747d29e7e6b39509b8f1847e37a23a0163ea6a", + "reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a", "shasum": "" }, "require": { @@ -176,12 +176,12 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "11.1.0", + "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2022.3", - "phpstan/phpstan": "1.10.9", + "phpstan/phpstan": "1.10.14", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.6", + "phpunit/phpunit": "9.6.7", "psalm/plugin-phpunit": "0.18.4", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^5.4|^6.0", @@ -246,7 +246,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.2" + "source": "https://github.com/doctrine/dbal/tree/3.6.3" }, "funding": [ { @@ -262,29 +262,33 @@ "type": "tidelift" } ], - "time": "2023-04-14T07:25:38+00:00" + "time": "2023-06-01T05:46:46+00:00" }, { "name": "doctrine/deprecations", - "version": "v1.1.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "8cffffb2218e01f3b370bf763e00e81697725259" + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/8cffffb2218e01f3b370bf763e00e81697725259", - "reference": "8cffffb2218e01f3b370bf763e00e81697725259", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5|^8.5|^9.5", - "psr/log": "^1|^2|^3" + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -303,9 +307,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.1.0" + "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" }, - "time": "2023-05-29T18:55:17+00:00" + "time": "2023-06-03T09:27:29+00:00" }, { "name": "doctrine/event-manager", @@ -934,16 +938,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.3.5", + "version": "1.3.6", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd" + "reference": "90d087e988ff194065333d16bc5cf649872d9cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/74780ccf8c19d6acb8d65c5f39cd72110e132bbd", - "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/90d087e988ff194065333d16bc5cf649872d9cdb", + "reference": "90d087e988ff194065333d16bc5cf649872d9cdb", "shasum": "" }, "require": { @@ -990,7 +994,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.5" + "source": "https://github.com/composer/ca-bundle/tree/1.3.6" }, "funding": [ { @@ -1006,7 +1010,7 @@ "type": "tidelift" } ], - "time": "2023-01-11T08:27:00+00:00" + "time": "2023-06-06T12:02:59+00:00" }, { "name": "composer/class-map-generator", @@ -4211,22 +4215,23 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.21.3", + "version": "1.22.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b0c366dd2cea79407d635839d25423ba07c55dd6" + "reference": "ec58baf7b3c7f1c81b3b00617c953249fb8cf30c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b0c366dd2cea79407d635839d25423ba07c55dd6", - "reference": "b0c366dd2cea79407d635839d25423ba07c55dd6", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/ec58baf7b3c7f1c81b3b00617c953249fb8cf30c", + "reference": "ec58baf7b3c7f1c81b3b00617c953249fb8cf30c", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", @@ -4251,9 +4256,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.21.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.22.0" }, - "time": "2023-05-29T19:31:28+00:00" + "time": "2023-06-01T12:35:21+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/public/assets/css/src/icons/no-image.svg b/public/assets/css/src/icons/no-image.svg new file mode 100644 index 0000000..3744910 --- /dev/null +++ b/public/assets/css/src/icons/no-image.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><title>no-image</title><path d="M30,3.4141,28.5859,2,2,28.5859,3.4141,30l2-2H26a2.0027,2.0027,0,0,0,2-2V5.4141ZM26,26H7.4141l7.7929-7.793,2.3788,2.3787a2,2,0,0,0,2.8284,0L22,19l4,3.9973Zm0-5.8318-2.5858-2.5859a2,2,0,0,0-2.8284,0L19,19.1682l-2.377-2.3771L26,7.4141Z"/><path d="M6,22V19l5-4.9966,1.3733,1.3733,1.4159-1.416-1.375-1.375a2,2,0,0,0-2.8284,0L6,16.1716V6H22V4H6A2.002,2.002,0,0,0,4,6V22Z"/><rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/></svg> \ No newline at end of file diff --git a/src/Entities/Bases/AbstractProvider.php b/src/Entities/Bases/AbstractProvider.php index 9e63abd..e02dc39 100755 --- a/src/Entities/Bases/AbstractProvider.php +++ b/src/Entities/Bases/AbstractProvider.php @@ -6,35 +6,43 @@ namespace SimpleSAML\Module\accounting\Entities\Bases; use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; use SimpleSAML\Module\accounting\Entities\Interfaces\ProviderInterface; +use SimpleSAML\Module\accounting\Services\HelpersManager; abstract class AbstractProvider implements ProviderInterface { protected array $metadata; + protected HelpersManager $helpersManager; protected string $entityId; - public function __construct(array $metadata) + public function __construct(array $metadata, HelpersManager $helpersManager = null) { $this->metadata = $metadata; + $this->helpersManager = $helpersManager ?? new HelpersManager(); $this->entityId = $this->resolveEntityId(); } - protected function resolveOptionallyLocalizedString(string $key, string $locale = 'en'): ?string - { - if (!isset($this->metadata[$key])) { + protected function resolveOptionallyLocalizedString( + string $key, + string $locale = self::DEFAULT_LOCALE, + array $metadataOverride = null + ): ?string { + $metadata = $metadataOverride ?? $this->metadata; + + if (!isset($metadata[$key])) { return null; } // Check for non-localized version. - if (is_string($this->metadata[$key])) { - return $this->metadata[$key]; + if (is_string($metadata[$key])) { + return $metadata[$key]; } if ( - is_array($this->metadata[$key]) && - !empty($this->metadata[$key][$locale]) && - is_string($this->metadata[$key][$locale]) + is_array($metadata[$key]) && + !empty($metadata[$key][$locale]) && + is_string($metadata[$key][$locale]) ) { - return $this->metadata[$key][$locale]; + return $metadata[$key][$locale]; } return null; @@ -50,8 +58,10 @@ abstract class AbstractProvider implements ProviderInterface return $this->entityId; } - abstract public function getName(string $locale = 'en'): ?string; - abstract public function getDescription(string $locale = 'en'): ?string; + abstract public function getName(string $locale = self::DEFAULT_LOCALE): ?string; + abstract public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string; + abstract public function getLogoUrl(): ?string; abstract protected function resolveEntityId(): string; abstract public function getProtocol(): AuthenticationProtocolInterface; + abstract protected function getProviderDescription(): string; } diff --git a/src/Entities/Interfaces/ProviderInterface.php b/src/Entities/Interfaces/ProviderInterface.php index c82a179..259db35 100755 --- a/src/Entities/Interfaces/ProviderInterface.php +++ b/src/Entities/Interfaces/ProviderInterface.php @@ -9,9 +9,12 @@ use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; interface ProviderInterface { + public const DEFAULT_LOCALE = 'en'; + public function getMetadata(): array; - public function getName(string $locale = 'en'): ?string; + public function getName(string $locale = self::DEFAULT_LOCALE): ?string; public function getEntityId(): string; - public function getDescription(string $locale = 'en'): ?string; + public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string; + public function getLogoUrl(): ?string; public function getProtocol(): AuthenticationProtocolInterface; } diff --git a/src/Entities/Providers/Bases/AbstractOidcProvider.php b/src/Entities/Providers/Bases/AbstractOidcProvider.php new file mode 100644 index 0000000..5c1e73e --- /dev/null +++ b/src/Entities/Providers/Bases/AbstractOidcProvider.php @@ -0,0 +1,34 @@ +<?php + +namespace SimpleSAML\Module\accounting\Entities\Providers\Bases; + +use SimpleSAML\Module\accounting\Entities\Authentication; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; +use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; + +abstract class AbstractOidcProvider extends AbstractProvider +{ + public const METADATA_KEY_LOGO_URI = 'logo_uri'; + + abstract public function getName(string $locale = self::DEFAULT_LOCALE): ?string; + abstract public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string; + abstract protected function resolveEntityId(): string; + abstract protected function getProviderDescription(): string; + + public function getLogoUrl(): ?string + { + if ( + !empty($this->metadata[self::METADATA_KEY_LOGO_URI]) && + is_string($this->metadata[self::METADATA_KEY_LOGO_URI]) + ) { + return $this->metadata[self::METADATA_KEY_LOGO_URI]; + } + + return null; + } + + public function getProtocol(): AuthenticationProtocolInterface + { + return new Authentication\Protocol\Oidc(); + } +} diff --git a/src/Entities/Providers/Bases/AbstractSaml2Provider.php b/src/Entities/Providers/Bases/AbstractSaml2Provider.php new file mode 100644 index 0000000..3ffbdca --- /dev/null +++ b/src/Entities/Providers/Bases/AbstractSaml2Provider.php @@ -0,0 +1,95 @@ +<?php + +namespace SimpleSAML\Module\accounting\Entities\Providers\Bases; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; +use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; +use SimpleSAML\Module\accounting\Exceptions\MetadataException; +use SimpleSAML\Module\accounting\Entities\Authentication; + +abstract class AbstractSaml2Provider extends AbstractProvider +{ + public const METADATA_KEY_ENTITY_ID = 'entityid'; + public const METADATA_KEY_NAME = 'name'; + public const METADATA_KEY_DESCRIPTION = 'description'; + public const METADATA_KEY_UI_INFO = 'UIInfo'; + public const METADATA_KEY_UI_INFO_DESCRIPTION = 'Description'; + public const METADATA_KEY_UI_INFO_DISPLAY_NAME = 'DisplayName'; + public const METADATA_KEY_UI_INFO_LOGO = 'Logo'; + public const METADATA_KEY_UI_INFO_LOGO_URL = 'url'; + + protected function getEntityInfoString(string $key, string $locale = self::DEFAULT_LOCALE): ?string + { + return $this->resolveOptionallyLocalizedString($key, $locale); + } + + protected function getEntityUiInfoString(string $key, string $locale = self::DEFAULT_LOCALE): ?string + { + if ( + isset($this->metadata[self::METADATA_KEY_UI_INFO]) && + is_array($this->metadata[self::METADATA_KEY_UI_INFO]) + ) { + return $this->resolveOptionallyLocalizedString( + $key, + $locale, + $this->metadata[self::METADATA_KEY_UI_INFO] + ); + } + + return null; + } + + public function getName(string $locale = self::DEFAULT_LOCALE): ?string + { + return $this->getEntityInfoString(self::METADATA_KEY_NAME, $locale) ?? + $this->getEntityUiInfoString(self::METADATA_KEY_UI_INFO_DISPLAY_NAME, $locale); + } + + public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string + { + return $this->getEntityInfoString(self::METADATA_KEY_DESCRIPTION, $locale) ?? + $this->getEntityUiInfoString(self::METADATA_KEY_UI_INFO_DESCRIPTION, $locale); + } + + public function getLogoUrl(): ?string + { + $logoElement = $this->helpersManager->getArr()->getNestedElementByKey( + $this->metadata, + self::METADATA_KEY_UI_INFO, + self::METADATA_KEY_UI_INFO_LOGO, + 0, + self::METADATA_KEY_UI_INFO_LOGO_URL + ); + + if (!is_array($logoElement)) { + return null; + } + + /** @var mixed $logoUrl */ + $logoUrl = current($logoElement); + + if (is_string($logoUrl) && filter_var($logoUrl, FILTER_VALIDATE_URL)) { + return $logoUrl; + } + + return null; + } + + /** + * @throws MetadataException + */ + protected function resolveEntityId(): string + { + if (empty($entityId = $this->getEntityInfoString(self::METADATA_KEY_ENTITY_ID))) { + $message = sprintf('Provider metadata does not contain entity ID (%s).', $this->getProviderDescription()); + throw new MetadataException($message); + } + + return $entityId; + } + + public function getProtocol(): AuthenticationProtocolInterface + { + return new Authentication\Protocol\Saml2(); + } +} diff --git a/src/Entities/Providers/Identity/Oidc.php b/src/Entities/Providers/Identity/Oidc.php index c662f78..0ead0e2 100755 --- a/src/Entities/Providers/Identity/Oidc.php +++ b/src/Entities/Providers/Identity/Oidc.php @@ -4,13 +4,11 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Entities\Providers\Identity; -use SimpleSAML\Module\accounting\Entities\Authentication; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; -use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; use SimpleSAML\Module\accounting\Entities\Interfaces\IdentityProviderInterface; +use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider; use SimpleSAML\Module\accounting\Exceptions\MetadataException; -class Oidc extends AbstractProvider implements IdentityProviderInterface +class Oidc extends AbstractOidcProvider implements IdentityProviderInterface { public const METADATA_KEY_ENTITY_ID = 'issuer'; @@ -36,11 +34,11 @@ class Oidc extends AbstractProvider implements IdentityProviderInterface return $this->metadata[self::METADATA_KEY_ENTITY_ID]; } - throw new MetadataException('OpenID VersionedDataProvider metadata does not contain entity ID.'); + throw new MetadataException($this->getProviderDescription() . ' metadata does not contain entity ID.'); } - public function getProtocol(): AuthenticationProtocolInterface + protected function getProviderDescription(): string { - return new Authentication\Protocol\Oidc(); + return $this->getProtocol()->getDesignation() . ' OpenID Provider'; } } diff --git a/src/Entities/Providers/Identity/Saml2.php b/src/Entities/Providers/Identity/Saml2.php index 0476863..8bef30d 100755 --- a/src/Entities/Providers/Identity/Saml2.php +++ b/src/Entities/Providers/Identity/Saml2.php @@ -5,44 +5,15 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Entities\Providers\Identity; use SimpleSAML\Module\accounting\Entities\Authentication; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; use SimpleSAML\Module\accounting\Entities\Interfaces\IdentityProviderInterface; +use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider; use SimpleSAML\Module\accounting\Exceptions\MetadataException; -class Saml2 extends AbstractProvider implements IdentityProviderInterface +class Saml2 extends AbstractSaml2Provider implements IdentityProviderInterface { - public const METADATA_KEY_NAME = 'name'; - public const METADATA_KEY_ENTITY_ID = 'entityid'; - public const METADATA_KEY_DESCRIPTION = 'description'; - - public function getName(string $locale = 'en'): ?string - { - return $this->resolveOptionallyLocalizedString(self::METADATA_KEY_NAME, $locale); - } - - public function getDescription(string $locale = 'en'): ?string - { - return $this->resolveOptionallyLocalizedString(self::METADATA_KEY_DESCRIPTION, $locale); - } - - /** - * @throws MetadataException - */ - protected function resolveEntityId(): string - { - if ( - !empty($this->metadata[self::METADATA_KEY_ENTITY_ID]) && - is_string($this->metadata[self::METADATA_KEY_ENTITY_ID]) - ) { - return $this->metadata[self::METADATA_KEY_ENTITY_ID]; - } - - throw new MetadataException('Identity provider metadata does not contain entity ID.'); - } - - public function getProtocol(): AuthenticationProtocolInterface + protected function getProviderDescription(): string { - return new Authentication\Protocol\Saml2(); + return $this->getProtocol()->getDesignation() . ' Identity Provider'; } } diff --git a/src/Entities/Providers/Service/Oidc.php b/src/Entities/Providers/Service/Oidc.php index 5f5ff82..10cb030 100755 --- a/src/Entities/Providers/Service/Oidc.php +++ b/src/Entities/Providers/Service/Oidc.php @@ -4,13 +4,11 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Entities\Providers\Service; -use SimpleSAML\Module\accounting\Entities\Authentication; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; -use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; use SimpleSAML\Module\accounting\Entities\Interfaces\ServiceProviderInterface; +use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider; use SimpleSAML\Module\accounting\Exceptions\MetadataException; -class Oidc extends AbstractProvider implements ServiceProviderInterface +class Oidc extends AbstractOidcProvider implements ServiceProviderInterface { public const METADATA_KEY_ENTITY_ID = 'id'; public const METADATA_KEY_NAME = 'name'; @@ -38,11 +36,11 @@ class Oidc extends AbstractProvider implements ServiceProviderInterface return $this->metadata[self::METADATA_KEY_ENTITY_ID]; } - throw new MetadataException('Relying VersionedDataProvider metadata does not contain entity ID.'); + throw new MetadataException($this->getProviderDescription() . ' metadata does not contain entity ID.'); } - public function getProtocol(): AuthenticationProtocolInterface + protected function getProviderDescription(): string { - return new Authentication\Protocol\Oidc(); + return $this->getProtocol()->getDesignation() . ' Relying Party'; } } diff --git a/src/Entities/Providers/Service/Saml2.php b/src/Entities/Providers/Service/Saml2.php index d0dfd4a..deeb307 100755 --- a/src/Entities/Providers/Service/Saml2.php +++ b/src/Entities/Providers/Service/Saml2.php @@ -4,45 +4,13 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Entities\Providers\Service; -use SimpleSAML\Module\accounting\Entities\Authentication; -use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider; -use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; use SimpleSAML\Module\accounting\Entities\Interfaces\ServiceProviderInterface; -use SimpleSAML\Module\accounting\Exceptions\MetadataException; +use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider; -class Saml2 extends AbstractProvider implements ServiceProviderInterface +class Saml2 extends AbstractSaml2Provider implements ServiceProviderInterface { - public const METADATA_KEY_ENTITY_ID = 'entityid'; - public const METADATA_KEY_NAME = 'name'; - public const METADATA_KEY_DESCRIPTION = 'description'; - - public function getName(string $locale = 'en'): ?string - { - return $this->resolveOptionallyLocalizedString(self::METADATA_KEY_NAME, $locale); - } - - public function getDescription(string $locale = 'en'): ?string - { - return $this->resolveOptionallyLocalizedString(self::METADATA_KEY_DESCRIPTION, $locale); - } - - /** - * @throws MetadataException - */ - protected function resolveEntityId(): string - { - if ( - !empty($this->metadata[self::METADATA_KEY_ENTITY_ID]) && - is_string($this->metadata[self::METADATA_KEY_ENTITY_ID]) - ) { - return $this->metadata[self::METADATA_KEY_ENTITY_ID]; - } - - throw new MetadataException('Service provider metadata does not contain entity ID.'); - } - - public function getProtocol(): AuthenticationProtocolInterface + protected function getProviderDescription(): string { - return new Authentication\Protocol\Saml2(); + return $this->getProtocol()->getDesignation() . ' Service Provider'; } } diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index 465e122..061461f 100755 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -37,4 +37,33 @@ class Arr $keys = array_keys($array); return $keys !== array_keys($keys); } + + /** + * @param array $array + * @param string|int ...$keys + * @return array|null + */ + public function getNestedElementByKey(array $array, ...$keys): ?array + { + $element = $array; + + foreach ($keys as $key) { + if (!is_array($element)) { + return null; + } + + if (!isset($element[$key])) { + return null; + } + + /** @var mixed $element */ + $element = $element[$key]; + } + + if (is_array($element)) { + return $element; + } + + return [$element]; + } } diff --git a/templates/user/connected-organizations.twig b/templates/user/connected-organizations.twig index 6bbbf8a..3e8e3af 100755 --- a/templates/user/connected-organizations.twig +++ b/templates/user/connected-organizations.twig @@ -20,7 +20,14 @@ {% for connectedServiceProvider in connectedServiceProviderBag.getAll %} <tr id="connected-service-provider-{{ loop.index }}"> - <td>{{ connectedServiceProvider.getServiceProvider.getName|e }}</td> + <td> + <img src="{{ connectedServiceProvider.serviceProvider.logoUrl ?? asset('css/src/icons/no-image.svg', 'accounting') }}" + width="30" + height="30" + loading="lazy" + alt="Service Logo"/> + {{ connectedServiceProvider.getServiceProvider.getName|e }} + </td> <td>{{ connectedServiceProvider.getNumberOfAuthentications|e }}</td> <td>{{ connectedServiceProvider.getLastAuthenticationAt|date() }}</td> </tr> diff --git a/tests/src/Constants/StateArrays.php b/tests/src/Constants/StateArrays.php index 1833f18..8b4b425 100755 --- a/tests/src/Constants/StateArrays.php +++ b/tests/src/Constants/StateArrays.php @@ -44,7 +44,22 @@ final class StateArrays 'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', 'metadata-set' => 'saml20-sp-remote', 'name' => 'Test service', - 'description' => 'Test service description' + 'description' => 'Test service description', + 'UIInfo' => [ + 'DisplayName' => [ + 'en' => 'Test service UiInfo', + ], + 'Description' => [ + 'en' => 'Test service description UiInfo', + ], + 'Logo' => [ + [ + 'url' => 'https://placehold.co/100x80/orange/white?text=test', + 'height' => 80, + 'width' => 100, + ], + ], + ], ], 'saml:RelayState' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp', 'saml:RequestId' => null, @@ -117,6 +132,21 @@ final class StateArrays 'entityid' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', 'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', 'metadata-set' => 'saml20-sp-remote', + 'UIInfo' => [ + 'DisplayName' => [ + 'en' => 'Test service UiInfo', + ], + 'Description' => [ + 'en' => 'Test service description UiInfo', + ], + 'Logo' => [ + [ + 'url' => 'https://placehold.co/100x80/orange/white?text=test', + 'height' => 80, + 'width' => 100, + ], + ], + ], ], 'Source' => [ 'host' => 'localhost.someone.from.hr', @@ -219,6 +249,7 @@ final class StateArrays 'owner' => null, 'post_logout_redirect_uri' => [], 'backchannel_logout_uri' => 'http://sp.host.internal:8074/logout.php', + 'logo_uri' => 'http://sp.host.internal:8074/logo.svg', ], 'AuthorizationRequestParameters' => [ 'response_type' => 'code', diff --git a/tests/src/Entities/Bases/AbstractProviderTest.php b/tests/src/Entities/Bases/AbstractProviderTest.php index e8d8a27..aae51bd 100755 --- a/tests/src/Entities/Bases/AbstractProviderTest.php +++ b/tests/src/Entities/Bases/AbstractProviderTest.php @@ -112,6 +112,16 @@ class AbstractProviderTest extends TestCase } }; } + + protected function getProviderDescription(): string + { + return 'provider description'; + } + + public function getLogoUrl(): ?string + { + return 'https://example.org/logo'; + } }; } } diff --git a/tests/src/Entities/Providers/Bases/AbstractOidcProviderTest.php b/tests/src/Entities/Providers/Bases/AbstractOidcProviderTest.php new file mode 100644 index 0000000..40dc7dc --- /dev/null +++ b/tests/src/Entities/Providers/Bases/AbstractOidcProviderTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Entities\Providers\Bases; + +use PHPUnit\Framework\MockObject\Stub; +use SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Oidc; +use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Helpers\Arr; +use SimpleSAML\Module\accounting\Services\HelpersManager; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + */ +class AbstractOidcProviderTest extends TestCase +{ + protected Stub $helpersManagerStub; + protected Stub $arrStub; + protected array $metadata; + + protected function setUp(): void + { + $this->helpersManagerStub = $this->createStub(HelpersManager::class); + $this->arrStub = $this->createStub(Arr::class); + $this->metadata = StateArrays::OIDC_FULL['Oidc']['RelyingPartyMetadata']; + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(AbstractOidcProvider::class, $this->prepareInstance(StateArrays::OIDC_FULL)); + } + + public function testCanGetLogoUrl(): void + { + $this->assertSame( + $this->metadata['logo_uri'], + $this->prepareInstance($this->metadata)->getLogoUrl() + ); + + $metadaNoLogo = $this->metadata; + unset($metadaNoLogo['logo_uri']); + + $this->assertNull($this->prepareInstance($metadaNoLogo)->getLogoUrl()); + } + + public function testCanGetProtocol(): void + { + $this->assertInstanceOf(Oidc::class, $this->prepareInstance($this->metadata)->getProtocol()); + } + + protected function prepareInstance(array $metadata): AbstractOidcProvider + { + $this->helpersManagerStub->method('getArr')->willReturn($this->arrStub); + + return new class ($metadata, $this->helpersManagerStub) extends AbstractOidcProvider { + public function getName(string $locale = self::DEFAULT_LOCALE): ?string + { + return 'name'; + } + + public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string + { + return 'description'; + } + + protected function resolveEntityId(): string + { + return 'entityId'; + } + + protected function getProviderDescription(): string + { + return 'provider description'; + } + }; + } +} diff --git a/tests/src/Entities/Providers/Bases/AbstractSaml2ProviderTest.php b/tests/src/Entities/Providers/Bases/AbstractSaml2ProviderTest.php new file mode 100644 index 0000000..0822ec2 --- /dev/null +++ b/tests/src/Entities/Providers/Bases/AbstractSaml2ProviderTest.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Entities\Providers\Bases; + +use PHPUnit\Framework\MockObject\Stub; +use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface; +use SimpleSAML\Module\accounting\Entities\Interfaces\ProviderInterface; +use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Exceptions\MetadataException; +use SimpleSAML\Module\accounting\Helpers\Arr; +use SimpleSAML\Module\accounting\Services\HelpersManager; +use SimpleSAML\Test\Module\accounting\Constants\StateArrays; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + */ +class AbstractSaml2ProviderTest extends TestCase +{ + protected Stub $helpersManagerStub; + protected Stub $arrStub; + protected array $metadata; + + protected function setUp(): void + { + $this->helpersManagerStub = $this->createStub(HelpersManager::class); + $this->arrStub = $this->createStub(Arr::class); + $this->metadata = StateArrays::SAML2_FULL['SPMetadata']; + } + + protected function prepareInstance(array $metadata): AbstractSaml2Provider + { + $this->helpersManagerStub->method('getArr')->willReturn($this->arrStub); + + return new class ($metadata, $this->helpersManagerStub) extends AbstractSaml2Provider { + protected function getProviderDescription(): string + { + return 'service description'; + } + }; + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(AbstractSaml2Provider::class, $this->prepareInstance($this->metadata)); + } + + public function testCanGetName(): void + { + $metadata = $this->metadata; + $this->assertSame( + $this->metadata[AbstractSaml2Provider::METADATA_KEY_NAME], + $this->prepareInstance($metadata)->getName() + ); + + unset($metadata[AbstractSaml2Provider::METADATA_KEY_NAME]); + $this->assertSame( + $this->metadata[AbstractSaml2Provider::METADATA_KEY_UI_INFO] + [AbstractSaml2Provider::METADATA_KEY_UI_INFO_DISPLAY_NAME] + [ProviderInterface::DEFAULT_LOCALE], + $this->prepareInstance($metadata)->getName() + ); + + unset($metadata[AbstractSaml2Provider::METADATA_KEY_UI_INFO]); + $this->assertNull($this->prepareInstance($metadata)->getName()); + } + + public function testCanGetDescription(): void + { + $metadata = $this->metadata; + $this->assertSame( + $this->metadata[AbstractSaml2Provider::METADATA_KEY_DESCRIPTION], + $this->prepareInstance($metadata)->getDescription() + ); + + unset($metadata[AbstractSaml2Provider::METADATA_KEY_DESCRIPTION]); + $this->assertSame( + $this->metadata[AbstractSaml2Provider::METADATA_KEY_UI_INFO] + [AbstractSaml2Provider::METADATA_KEY_UI_INFO_DESCRIPTION] + [ProviderInterface::DEFAULT_LOCALE], + $this->prepareInstance($metadata)->getDescription() + ); + + unset($metadata[AbstractSaml2Provider::METADATA_KEY_UI_INFO]); + $this->assertNull($this->prepareInstance($metadata)->getDescription()); + } + + public function testCanGetLogo(): void + { + $this->arrStub->method('getNestedElementByKey')->willReturn(['https://example.org/logo']); + + $this->assertSame( + 'https://example.org/logo', + $this->prepareInstance($this->metadata)->getLogoUrl() + ); + } + + public function testGetLogoNotFound(): void + { + $this->arrStub->method('getNestedElementByKey')->willReturn(null); + + $this->assertNull( + $this->prepareInstance($this->metadata)->getLogoUrl() + ); + } + + public function testGetLogoIsNullIfNotValid(): void + { + $this->arrStub->method('getNestedElementByKey')->willReturn(['not-valid']); + + $this->assertNull( + $this->prepareInstance($this->metadata)->getLogoUrl() + ); + } + + public function testThrowsForInvalidEntityId(): void + { + $metadata = $this->metadata; + unset($metadata[AbstractSaml2Provider::METADATA_KEY_ENTITY_ID]); + + $this->expectException(MetadataException::class); + + $this->prepareInstance($metadata); + } + + public function testCanGetProtocol(): void + { + $this->assertInstanceOf( + AuthenticationProtocolInterface::class, + $this->prepareInstance($this->metadata)->getProtocol() + ); + } +} diff --git a/tests/src/Entities/Providers/Identity/OidcTest.php b/tests/src/Entities/Providers/Identity/OidcTest.php index 91ab3e2..32591e2 100755 --- a/tests/src/Entities/Providers/Identity/OidcTest.php +++ b/tests/src/Entities/Providers/Identity/OidcTest.php @@ -11,6 +11,8 @@ use SimpleSAML\Module\accounting\Exceptions\MetadataException; /** * @covers \SimpleSAML\Module\accounting\Entities\Providers\Identity\Oidc * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Oidc */ class OidcTest extends TestCase { diff --git a/tests/src/Entities/Providers/Identity/Saml2Test.php b/tests/src/Entities/Providers/Identity/Saml2Test.php index 7c99595..5c4db09 100755 --- a/tests/src/Entities/Providers/Identity/Saml2Test.php +++ b/tests/src/Entities/Providers/Identity/Saml2Test.php @@ -12,6 +12,8 @@ use SimpleSAML\Test\Module\accounting\Constants\StateArrays; /** * @covers \SimpleSAML\Module\accounting\Entities\Providers\Identity\Saml2 * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Saml2 */ class Saml2Test extends TestCase { diff --git a/tests/src/Entities/Providers/Service/OidcTest.php b/tests/src/Entities/Providers/Service/OidcTest.php index db62ad4..dbba56c 100755 --- a/tests/src/Entities/Providers/Service/OidcTest.php +++ b/tests/src/Entities/Providers/Service/OidcTest.php @@ -11,6 +11,8 @@ use SimpleSAML\Module\accounting\Exceptions\MetadataException; /** * @covers \SimpleSAML\Module\accounting\Entities\Providers\Service\Oidc * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Oidc */ class OidcTest extends TestCase { diff --git a/tests/src/Entities/Providers/Service/Saml2Test.php b/tests/src/Entities/Providers/Service/Saml2Test.php index c4475d7..225c92a 100755 --- a/tests/src/Entities/Providers/Service/Saml2Test.php +++ b/tests/src/Entities/Providers/Service/Saml2Test.php @@ -9,6 +9,8 @@ use SimpleSAML\Module\accounting\Exceptions\MetadataException; /** * @covers \SimpleSAML\Module\accounting\Entities\Providers\Service\Saml2 * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider + * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Saml2 */ class Saml2Test extends TestCase { diff --git a/tests/src/Helpers/ArrayTest.php b/tests/src/Helpers/ArrayTest.php index dc35b09..2ad8905 100755 --- a/tests/src/Helpers/ArrayTest.php +++ b/tests/src/Helpers/ArrayTest.php @@ -70,4 +70,37 @@ class ArrayTest extends TestCase $this->assertTrue((new Arr())->isAssociative($associative)); $this->assertTrue((new Arr())->isAssociative($mixed)); } + + public function testGetNestedElementByKey(): void + { + $simpleIndexed = [1, 2, 3]; + $nestedIndexed = [[1, [2, [4, [5]]]], [3, 4]]; + $nestedAssociative = ['a' => ['b' => 'c'], 'd' => 'e']; + + $arrHelper = new Arr(); + + $this->assertSame($arrHelper->getNestedElementByKey($simpleIndexed, 0), [1]); + $this->assertNull($arrHelper->getNestedElementByKey($simpleIndexed, 3)); + $this->assertNull($arrHelper->getNestedElementByKey($simpleIndexed, 'a')); + $this->assertNull($arrHelper->getNestedElementByKey($simpleIndexed, 0, 'a')); + + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0), [1, [2, [4, [5]]]]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0), [1, [2, [4, [5]]]]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 0), [1]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1), [2, [4, [5]]]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 0), [2]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 1), [4, [5]]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 1, 0), [4]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 1, 1), [5]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 1), [3, 4]); + $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 1, 0), [3]); + $this->assertNull($arrHelper->getNestedElementByKey($nestedIndexed, 3)); + $this->assertNull($arrHelper->getNestedElementByKey($nestedIndexed, 'a')); + + $this->assertSame($arrHelper->getNestedElementByKey($nestedAssociative, 'a'), ['b' => 'c']); + $this->assertSame($arrHelper->getNestedElementByKey($nestedAssociative, 'a', 'b'), ['c']); + $this->assertSame($arrHelper->getNestedElementByKey($nestedAssociative, 'd'), ['e']); + $this->assertNull($arrHelper->getNestedElementByKey($nestedAssociative, 3)); + $this->assertNull($arrHelper->getNestedElementByKey($nestedAssociative, 'f')); + } } diff --git a/tests/src/Helpers/ProviderResolverTest.php b/tests/src/Helpers/ProviderResolverTest.php index 3661701..854e1af 100755 --- a/tests/src/Helpers/ProviderResolverTest.php +++ b/tests/src/Helpers/ProviderResolverTest.php @@ -21,6 +21,10 @@ use SimpleSAML\Test\Module\accounting\Constants\StateArrays; * @uses \SimpleSAML\Module\accounting\Entities\Providers\Identity\Oidc * @uses \SimpleSAML\Module\accounting\Entities\Providers\Service\Saml2 * @uses \SimpleSAML\Module\accounting\Entities\Providers\Service\Oidc + * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractSaml2Provider + * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Saml2 + * @uses \SimpleSAML\Module\accounting\Entities\Authentication\Protocol\Oidc */ class ProviderResolverTest extends TestCase { -- GitLab