diff --git a/config/packages/dev/monolog.yaml b/config/packages/dev/monolog.yaml index b02340fed..3f8643390 100644 --- a/config/packages/dev/monolog.yaml +++ b/config/packages/dev/monolog.yaml @@ -8,6 +8,12 @@ monolog: handler: nested level: debug channels: ["!authentication"] + critical: + type: stream + level: error + path: "%kernel.logs_dir%/error.log" + formatter: monolog.formatter.line + channels: [ "!event" ] nested: type: stream level: debug diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index a2e1c5563..ceb365aec 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -23,6 +23,16 @@ doctrine: engineblock_collab_person_uuid: OpenConext\EngineBlockBundle\Doctrine\Type\CollabPersonUuidType engineblock_metadata_coins: OpenConext\EngineBlockBundle\Doctrine\Type\MetadataCoinType engineblock_metadata_mdui: OpenConext\EngineBlockBundle\Doctrine\Type\MetadataMduiType + engineblock_certificate_array: OpenConext\EngineBlockBundle\Doctrine\Type\CertificateArrayType + engineblock_attribute_release_policy: OpenConext\EngineBlockBundle\Doctrine\Type\AttributeReleasePolicyType + engineblock_contact_person_array: OpenConext\EngineBlockBundle\Doctrine\Type\ContactPersonArrayType + engineblock_indexed_service_array: OpenConext\EngineBlockBundle\Doctrine\Type\IndexedServiceArrayType + engineblock_logo: OpenConext\EngineBlockBundle\Doctrine\Type\LogoType + engineblock_organization: OpenConext\EngineBlockBundle\Doctrine\Type\OrganizationType + engineblock_requested_attribute_array: OpenConext\EngineBlockBundle\Doctrine\Type\RequestedAttributeArrayType + engineblock_service: OpenConext\EngineBlockBundle\Doctrine\Type\ServiceType + engineblock_service_array: OpenConext\EngineBlockBundle\Doctrine\Type\ServiceArrayType + engineblock_shib_md_scope_array: OpenConext\EngineBlockBundle\Doctrine\Type\ShibMdScopeArrayType array: OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType object: OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType diff --git a/config/reference.php b/config/reference.php index c7a371fb9..dcb968430 100644 --- a/config/reference.php +++ b/config/reference.php @@ -128,7 +128,7 @@ * @psalm-type FrameworkConfig = array{ * secret?: scalar|Param|null, * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false - * allowed_http_method_override?: list|null, + * allowed_http_method_override?: null|list, * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" * test?: bool|Param, @@ -136,9 +136,9 @@ * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false * enabled_locales?: list, - * trusted_hosts?: list, + * trusted_hosts?: string|list, * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] - * trusted_headers?: list, + * trusted_headers?: string|list, * error_controller?: scalar|Param|null, // Default: "error_controller" * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true * csrf_protection?: bool|array{ @@ -202,23 +202,23 @@ * property?: scalar|Param|null, * service?: scalar|Param|null, * }, - * supports?: list, + * supports?: string|list, * definition_validators?: list, * support_strategy?: scalar|Param|null, - * initial_marking?: list, - * events_to_dispatch?: list|null, - * places?: list, + * events_to_dispatch?: null|list, + * places?: string|list, * }>, * transitions?: list, - * to?: list, @@ -271,7 +271,7 @@ * version_format?: scalar|Param|null, // Default: "%%s?%%s" * json_manifest_path?: scalar|Param|null, // Default: null * base_path?: scalar|Param|null, // Default: "" - * base_urls?: list, + * base_urls?: string|list, * packages?: array, + * base_urls?: string|list, * }>, * }, * asset_mapper?: bool|array{ // Asset Mapper configuration * enabled?: bool|Param, // Default: false - * paths?: array, + * paths?: string|array, * excluded_patterns?: list, * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true @@ -303,7 +303,7 @@ * }, * translator?: bool|array{ // Translator configuration * enabled?: bool|Param, // Default: true - * fallbacks?: list, + * fallbacks?: string|list, * logging?: bool|Param, // Default: false * formatter?: scalar|Param|null, // Default: "translator.formatter.default" * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" @@ -333,7 +333,7 @@ * enabled?: bool|Param, // Default: true * cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0. * enable_attributes?: bool|Param, // Default: true - * static_method?: list, + * static_method?: string|list, * translation_domain?: scalar|Param|null, // Default: "validators" * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5" * mapping?: array{ @@ -396,7 +396,7 @@ * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" * default_pdo_provider?: scalar|Param|null, // Default: null * pools?: array, + * adapters?: string|list, * tags?: scalar|Param|null, // Default: null * public?: bool|Param, // Default: false * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. @@ -419,11 +419,11 @@ * }, * lock?: bool|string|array{ // Lock configuration * enabled?: bool|Param, // Default: false - * resources?: array>, + * resources?: string|array>, * }, * semaphore?: bool|string|array{ // Semaphore configuration * enabled?: bool|Param, // Default: false - * resources?: array, + * resources?: string|array, * }, * messenger?: bool|array{ // Messenger configuration * enabled?: bool|Param, // Default: false @@ -453,7 +453,7 @@ * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null * }>, * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null - * stop_worker_on_signals?: list, + * stop_worker_on_signals?: int|string|list, * default_bus?: scalar|Param|null, // Default: null * buses?: array, * }>, @@ -510,9 +510,9 @@ * retry_failed?: bool|array{ * enabled?: bool|Param, // Default: false * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null - * http_codes?: array, + * methods?: string|list, * }>, * max_retries?: int|Param, // Default: 3 * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 @@ -563,9 +563,9 @@ * retry_failed?: bool|array{ * enabled?: bool|Param, // Default: false * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null - * http_codes?: array, + * methods?: string|list, * }>, * max_retries?: int|Param, // Default: 3 * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 @@ -582,8 +582,8 @@ * transports?: array, * envelope?: array{ // Mailer Envelope configuration * sender?: scalar|Param|null, - * recipients?: list, - * allowed_recipients?: list, + * recipients?: string|list, + * allowed_recipients?: string|list, * }, * headers?: array, + * limiters?: string|list, * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". @@ -658,20 +658,20 @@ * allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false * allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false * allow_elements?: array, - * block_elements?: list, - * drop_elements?: list, + * block_elements?: string|list, + * drop_elements?: string|list, * allow_attributes?: array, * drop_attributes?: array, * force_attributes?: array>, * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false - * allowed_link_schemes?: list, - * allowed_link_hosts?: list|null, + * allowed_link_schemes?: string|list, + * allowed_link_hosts?: null|string|list, * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false - * allowed_media_schemes?: list, - * allowed_media_hosts?: list|null, + * allowed_media_schemes?: string|list, + * allowed_media_hosts?: null|string|list, * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false - * with_attribute_sanitizers?: list, - * without_attribute_sanitizers?: list, + * with_attribute_sanitizers?: string|list, + * without_attribute_sanitizers?: string|list, * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 * }>, * }, @@ -705,7 +705,7 @@ * }, * password_hashers?: array, + * migrate_from?: string|list, * hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" * key_length?: scalar|Param|null, // Default: 40 * ignore_case?: bool|Param, // Default: false @@ -719,12 +719,12 @@ * providers?: array, + * providers?: string|list, * }, * memory?: array{ * users?: array, + * roles?: string|list, * }>, * }, * ldap?: array{ @@ -733,7 +733,7 @@ * search_dn?: scalar|Param|null, // Default: null * search_password?: scalar|Param|null, // Default: null * extra_fields?: list, - * default_roles?: list, + * default_roles?: string|list, * role_fetcher?: scalar|Param|null, // Default: null * uid_key?: scalar|Param|null, // Default: "sAMAccountName" * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" @@ -748,7 +748,7 @@ * firewalls?: array, + * methods?: string|list, * security?: bool|Param, // Default: true * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" * request_matcher?: scalar|Param|null, @@ -767,8 +767,8 @@ * path?: scalar|Param|null, // Default: "/logout" * target?: scalar|Param|null, // Default: "/" * invalidate_session?: bool|Param, // Default: true - * clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, - * delete_cookies?: array, + * delete_cookies?: string|array, + * token_extractors?: string|list, * token_handler?: string|array{ * id?: scalar|Param|null, * oidc_user_info?: string|array{ @@ -921,7 +921,7 @@ * }, * oidc?: array{ * discovery?: array{ // Enable the OIDC discovery. - * base_uri?: list, + * base_uri?: string|list, * cache?: array{ * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. * }, @@ -964,7 +964,7 @@ * remember_me?: array{ * secret?: scalar|Param|null, // Default: "%kernel.secret%" * service?: scalar|Param|null, - * user_providers?: list, + * user_providers?: string|list, * catch_exceptions?: bool|Param, // Default: true * signature_properties?: list, * token_provider?: string|array{ @@ -992,12 +992,12 @@ * path?: scalar|Param|null, // Use the urldecoded format. // Default: null * host?: scalar|Param|null, // Default: null * port?: int|Param, // Default: null - * ips?: list, + * ips?: string|list, * attributes?: array, * route?: scalar|Param|null, // Default: null - * methods?: list, + * methods?: string|list, * allow_if?: scalar|Param|null, // Default: null - * roles?: list, + * roles?: string|list, * }>, * role_hierarchy?: array>, * } @@ -1018,7 +1018,7 @@ * auto_reload?: scalar|Param|null, * optimizations?: int|Param, * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" - * file_name_pattern?: list, + * file_name_pattern?: string|list, * paths?: array, * date?: array{ // The default format options used by the date filter. * format?: scalar|Param|null, // Default: "F j, Y H:i" @@ -1111,7 +1111,7 @@ * delay_between_messages?: bool|Param, // Default: false * topic?: int|Param, // Default: null * factor?: int|Param, // Default: 1 - * tags?: list, + * tags?: string|list, * console_formatter_options?: mixed, // Default: [] * formatter?: scalar|Param|null, * nested?: bool|Param, // Default: false @@ -1155,7 +1155,7 @@ * host?: scalar|Param|null, * }, * from_email?: scalar|Param|null, - * to_email?: list, + * to_email?: string|list, * subject?: scalar|Param|null, * content_type?: scalar|Param|null, // Default: null * headers?: list, @@ -1466,6 +1466,8 @@ * monolog?: MonologConfig, * doctrine?: DoctrineConfig, * doctrine_migrations?: DoctrineMigrationsConfig, + * debug?: DebugConfig, + * web_profiler?: WebProfilerConfig, * "when@ci"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1492,17 +1494,6 @@ * debug?: DebugConfig, * web_profiler?: WebProfilerConfig, * }, - * "when@prod"?: array{ - * imports?: ImportsConfig, - * parameters?: ParametersConfig, - * services?: ServicesConfig, - * framework?: FrameworkConfig, - * security?: SecurityConfig, - * twig?: TwigConfig, - * monolog?: MonologConfig, - * doctrine?: DoctrineConfig, - * doctrine_migrations?: DoctrineMigrationsConfig, - * }, * "when@test"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1601,7 +1592,6 @@ public static function config(array $config): array * @psalm-type RoutesConfig = array{ * "when@ci"?: array, * "when@dev"?: array, - * "when@prod"?: array, * "when@test"?: array, * ... * } diff --git a/migrations/DoctrineMigrations/Version20260210000000.php b/migrations/DoctrineMigrations/Version20260210000000.php index 3277a46df..d7afffb17 100644 --- a/migrations/DoctrineMigrations/Version20260210000000.php +++ b/migrations/DoctrineMigrations/Version20260210000000.php @@ -36,19 +36,14 @@ public function getDescription(): string return 'Baseline migration: Creates all database tables (consent, saml_persistent_id, service_provider_uuid, sso_provider_roles_eb5, user). Skips if tables already exist.'; } - public function preUp(Schema $schema): void + public function up(Schema $schema): void { - parent::preUp($schema); - $tables = $this->sm->listTableNames(); - $this->skipIf( - in_array('sso_provider_roles_eb5', $tables, true), - 'Database schema already exists (found sso_provider_roles_eb5 table). Skipping baseline migration.' - ); - } + // Database schema already exists (found sso_provider_roles_eb5 table). Skipping baseline migration + if (in_array('sso_provider_roles_eb5', $tables, true)) { + return; + } - public function up(Schema $schema): void - { $this->addSql('CREATE TABLE `consent` ( `consent_date` datetime NOT NULL, `hashed_user_id` varchar(80) NOT NULL, diff --git a/migrations/DoctrineMigrations/Version20260224000000.php b/migrations/DoctrineMigrations/Version20260224000000.php index e85a85b6e..00fb1a1ba 100644 --- a/migrations/DoctrineMigrations/Version20260224000000.php +++ b/migrations/DoctrineMigrations/Version20260224000000.php @@ -40,11 +40,17 @@ public function getDescription(): string return 'Patch migration: Removes the deleted_at index from the consent table. Skips if the index does not exist.'; } - public function preUp(Schema $schema): void + public function up(Schema $schema): void { - parent::preUp($schema); + $indexes = $this->connection + ->createSchemaManager() + ->introspectTableIndexes( + new OptionallyQualifiedName( + Identifier::unquoted('consent'), + null + ) + ); - $indexes = $this->connection->createSchemaManager()->introspectTableIndexes(new OptionallyQualifiedName(Identifier::unquoted('consent'), null)); $deletedAtIndex = array_filter( $indexes, static fn(Index $index) => $index->getObjectName()->equals( @@ -53,14 +59,11 @@ public function preUp(Schema $schema): void ) ); - $this->skipIf( - count($deletedAtIndex) === 0, - 'Index deleted_at on consent table does not exist. Skipping.' - ); - } + // Index deleted_at on consent table does not exist. Skipping + if (count($deletedAtIndex) === 0) { + return; + } - public function up(Schema $schema): void - { $this->addSql('ALTER TABLE `consent` DROP INDEX `deleted_at`'); } diff --git a/migrations/DoctrineMigrations/Version20260817133323.php b/migrations/DoctrineMigrations/Version20260817133323.php new file mode 100644 index 000000000..41af23698 --- /dev/null +++ b/migrations/DoctrineMigrations/Version20260817133323.php @@ -0,0 +1,80 @@ +addSql(<<<'SQL' + CREATE TABLE sso_provider_roles_eb6 ( + id INT AUTO_INCREMENT NOT NULL, + entity_id VARCHAR(255) NOT NULL, + name_nl VARCHAR(255), + name_en VARCHAR(255), + name_pt VARCHAR(255), + description_nl VARCHAR(255), + description_en VARCHAR(255), + description_pt VARCHAR(255), + display_name_nl VARCHAR(255), + display_name_en VARCHAR(255), + display_name_pt VARCHAR(255), + logo JSON, + organization_nl_name JSON DEFAULT NULL, + organization_en_name JSON DEFAULT NULL, + organization_pt_name JSON DEFAULT NULL, + keywords_nl VARCHAR(255), + keywords_en VARCHAR(255), + keywords_pt VARCHAR(255), + certificates JSON, + workflow_state VARCHAR(255) NOT NULL, + contact_persons JSON, + name_id_format VARCHAR(255) DEFAULT NULL, + name_id_formats JSON NOT NULL, + single_logout_service JSON DEFAULT NULL, + requests_must_be_signed TINYINT NOT NULL, + manipulation TEXT, + coins JSON NOT NULL, + mdui JSON NOT NULL, + type VARCHAR(255) NOT NULL, + attribute_release_policy JSON DEFAULT NULL, + assertion_consumer_services JSON DEFAULT NULL, + allowed_idp_entity_ids JSON DEFAULT NULL, + allow_all TINYINT DEFAULT NULL, + requested_attributes JSON DEFAULT NULL, + support_url_en VARCHAR(255) DEFAULT NULL, + support_url_nl VARCHAR(255) DEFAULT NULL, + support_url_pt VARCHAR(255) DEFAULT NULL, + enabled_in_wayf TINYINT DEFAULT NULL, + single_sign_on_services JSON DEFAULT NULL, + consent_settings LONGTEXT DEFAULT NULL, + shib_md_scopes JSON DEFAULT NULL, + idp_discoveries JSON DEFAULT NULL, + INDEX idx_sso_provider_roles_type (type), + INDEX idx_sso_provider_roles_entity_id (entity_id), + UNIQUE INDEX idx_sso_provider_roles_entity_id_type (type, entity_id), + PRIMARY KEY (id) + ) DEFAULT CHARACTER SET UTF8 + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE sso_provider_roles_eb6'); + } +} diff --git a/src/OpenConext/EngineBlock/Metadata/AttributeReleasePolicy.php b/src/OpenConext/EngineBlock/Metadata/AttributeReleasePolicy.php index c202b7937..4fd89e4b9 100644 --- a/src/OpenConext/EngineBlock/Metadata/AttributeReleasePolicy.php +++ b/src/OpenConext/EngineBlock/Metadata/AttributeReleasePolicy.php @@ -23,7 +23,7 @@ class AttributeReleasePolicy { - const WILDCARD_CHARACTER = '*'; + const string WILDCARD_CHARACTER = '*'; /** * Holds attribute rule values with optional 'source'. @@ -40,11 +40,8 @@ class AttributeReleasePolicy * * @var array */ - private $attributeRules; + private array $attributeRules; - /** - * @param array $attributeRules - */ public function __construct(array $attributeRules) { foreach ($attributeRules as $key => $rules) { @@ -67,11 +64,9 @@ public function __construct(array $attributeRules) } /** - * @param string $key - * @param mixed $rule * @throws InvalidArgumentException */ - private function validateRule($key, $rule) + private function validateRule(string $key, mixed $rule): void { if (is_array($rule)) { if (!isset($rule['value'])) { @@ -110,8 +105,6 @@ private function validateRule($key, $rule) * Return all attribute rules eligible for attribute aggregation. * * A rule is eligible for attribute aggregation if it contains a source. - * - * @return array */ public function getRulesWithSourceSpecification(): array { @@ -160,10 +153,7 @@ public function findNameIdSubstitute(): ?string return null; } - /** - * @return array - */ - public function getAttributeNames() + public function getAttributeNames(): array { return array_keys($this->attributeRules); } @@ -172,7 +162,7 @@ public function getAttributeNames() * @param $attributeName * @return bool */ - public function hasAttribute($attributeName) + public function hasAttribute($attributeName): bool { return isset($this->attributeRules[$attributeName]); } @@ -182,7 +172,7 @@ public function hasAttribute($attributeName) * @param $attributeValue * @return bool */ - public function isAllowed($attributeName, $attributeValue) + public function isAllowed($attributeName, $attributeValue): bool { if (!$this->hasAttribute($attributeName)) { return false; @@ -224,7 +214,7 @@ public function isAllowed($attributeName, $attributeValue) * @param $rule * @return string */ - private function getRuleValue($rule) + private function getRuleValue($rule): string { if (isset($rule['value'])) { return (string) $rule['value']; @@ -237,16 +227,16 @@ private function getRuleValue($rule) * Loads the motivation text for an attribute. * * @param $attributeName - * @return string + * @return ?string */ - public function getMotivation($attributeName) + public function getMotivation($attributeName): ?string { if (!$this->hasAttribute($attributeName)) { - return; + return null; } if (empty($this->attributeRules[$attributeName][0]['motivation'])) { - return; + return null; } return $this->attributeRules[$attributeName][0]['motivation']; @@ -258,7 +248,7 @@ public function getMotivation($attributeName) * @param $attributeName * @return string */ - public function getSource($attributeName) + public function getSource($attributeName): string { if ($this->hasAttribute($attributeName) && isset($this->attributeRules[$attributeName][0]['source'])) { return $this->attributeRules[$attributeName][0]['source']; @@ -266,11 +256,16 @@ public function getSource($attributeName) return 'idp'; } + public function getAttributeRules(): array + { + return $this->attributeRules; + } + /** - * @return array + * A convenience static constructor for the AttributeReleasePolicy. */ - public function getAttributeRules() + public static function fromArray(array $attributeReleasePolicy): AttributeReleasePolicy { - return $this->attributeRules; + return new self($attributeReleasePolicy); } } diff --git a/src/OpenConext/EngineBlock/Metadata/ContactPerson.php b/src/OpenConext/EngineBlock/Metadata/ContactPerson.php index 9f49cd487..ea7bcf593 100644 --- a/src/OpenConext/EngineBlock/Metadata/ContactPerson.php +++ b/src/OpenConext/EngineBlock/Metadata/ContactPerson.php @@ -24,18 +24,31 @@ */ class ContactPerson { - public $contactType; - public $emailAddress = ''; - public $telephoneNumber = ''; - public $givenName = ''; - public $surName = ''; + public string $contactType; + public string $emailAddress; + public string $telephoneNumber; + public string $givenName; + public string $surName; /** * @param $contactType + * @param string $emailAddress + * @param string $telephoneNumber + * @param string $givenName + * @param string $surName */ - public function __construct($contactType) - { + public function __construct( + $contactType, + string $emailAddress = '', + string $telephoneNumber = '', + string $givenName = '', + string $surName = '' + ) { $this->contactType = $contactType; + $this->emailAddress = $emailAddress; + $this->telephoneNumber = $telephoneNumber; + $this->givenName = $givenName; + $this->surName = $surName; } /** @@ -61,4 +74,19 @@ public static function from( $contact->telephoneNumber = $telephoneNumber; return $contact; } + + + /** + * A convenience static constructor for the contact person. + */ + public static function fromArray(array $contactPerson): ContactPerson + { + return new self( + $contactPerson["contactType"], + $contactPerson["emailAddress"], + $contactPerson["telephoneNumber"], + $contactPerson["givenName"], + $contactPerson["surName"] + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php index 62723b0d3..226206ee8 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php @@ -28,8 +28,11 @@ use OpenConext\EngineBlock\Metadata\Organization; use OpenConext\EngineBlock\Metadata\Service; use OpenConext\EngineBlock\Metadata\X509\X509Certificate; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType; +use OpenConext\EngineBlockBundle\Doctrine\Type\CertificateArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\ContactPersonArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\LogoType; +use OpenConext\EngineBlockBundle\Doctrine\Type\OrganizationType; +use OpenConext\EngineBlockBundle\Doctrine\Type\ServiceType; use RuntimeException; use SAML2\Constants; @@ -51,15 +54,16 @@ #[ORM\InheritanceType('SINGLE_TABLE')] #[ORM\DiscriminatorColumn(name: 'type', type: 'string')] #[ORM\DiscriminatorMap(['sp' => ServiceProvider::class, 'idp' => IdentityProvider::class])] -#[ORM\Table(name: 'sso_provider_roles_eb5')] +#[ORM\Table(name: 'sso_provider_roles_eb6')] #[ORM\Index(name: 'idx_sso_provider_roles_type', columns: ['type'])] #[ORM\Index(name: 'idx_sso_provider_roles_entity_id', columns: ['entity_id'])] #[ORM\UniqueConstraint(name: 'idx_sso_provider_roles_entity_id_type', columns: ['type', 'entity_id'])] abstract class AbstractRole { - const WORKFLOW_STATE_PROD = 'prodaccepted'; - const WORKFLOW_STATE_TEST = 'testaccepted'; - const WORKFLOW_STATE_DEFAULT = self::WORKFLOW_STATE_PROD; + const string TABLE_NAME = 'sso_provider_roles_eb6'; + const string WORKFLOW_STATE_PROD = 'prodaccepted'; + const string WORKFLOW_STATE_TEST = 'testaccepted'; + const string WORKFLOW_STATE_DEFAULT = self::WORKFLOW_STATE_PROD; /** * @var int @@ -78,126 +82,126 @@ abstract class AbstractRole /** * @var string */ - #[ORM\Column(name: 'name_nl', type: Types::STRING)] + #[ORM\Column(name: 'name_nl', type: Types::STRING, nullable: true)] public ?string $nameNl = null; /** * @var string */ - #[ORM\Column(name: 'name_en', type: Types::STRING)] + #[ORM\Column(name: 'name_en', type: Types::STRING, nullable: true)] public ?string $nameEn = null; /** * @var string */ - #[ORM\Column(name: 'name_pt', type: Types::STRING)] + #[ORM\Column(name: 'name_pt', type: Types::STRING, nullable: true)] public ?string $namePt = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'description_nl', type: Types::STRING)] + #[ORM\Column(name: 'description_nl', type: Types::STRING, nullable: true)] public ?string $descriptionNl = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'description_en', type: Types::STRING)] + #[ORM\Column(name: 'description_en', type: Types::STRING, nullable: true)] public ?string $descriptionEn = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'description_pt', type: Types::STRING)] + #[ORM\Column(name: 'description_pt', type: Types::STRING, nullable: true)] public ?string $descriptionPt = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'display_name_nl', type: Types::STRING)] + #[ORM\Column(name: 'display_name_nl', type: Types::STRING, nullable: true)] public ?string $displayNameNl = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'display_name_en', type: Types::STRING)] + #[ORM\Column(name: 'display_name_en', type: Types::STRING, nullable: true)] public ?string $displayNameEn = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'display_name_pt', type: Types::STRING)] + #[ORM\Column(name: 'display_name_pt', type: Types::STRING, nullable: true)] public ?string $displayNamePt = null; /** * @var Logo * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'logo', type: SerializedObjectType::NAME)] - public $logo; + #[ORM\Column(name: 'logo', type: LogoType::NAME, nullable: true)] + public ?Logo $logo; /** * @var Organization */ - #[ORM\Column(name: 'organization_nl_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $organizationNl; + #[ORM\Column(name: 'organization_nl_name', type: OrganizationType::NAME, length: 65535, nullable: true)] + public ?Organization $organizationNl; /** * @var Organization */ - #[ORM\Column(name: 'organization_en_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $organizationEn; + #[ORM\Column(name: 'organization_en_name', type: OrganizationType::NAME, length: 65535, nullable: true)] + public ?Organization $organizationEn; /** * @var Organization */ - #[ORM\Column(name: 'organization_pt_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $organizationPt; + #[ORM\Column(name: 'organization_pt_name', type: OrganizationType::NAME, length: 65535, nullable: true)] + public ?Organization $organizationPt; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'keywords_nl', type: Types::STRING)] + #[ORM\Column(name: 'keywords_nl', type: Types::STRING, nullable: true)] public ?string $keywordsNl = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'keywords_en', type: Types::STRING)] + #[ORM\Column(name: 'keywords_en', type: Types::STRING, nullable: true)] public ?string $keywordsEn = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'keywords_pt', type: Types::STRING)] + #[ORM\Column(name: 'keywords_pt', type: Types::STRING, nullable: true)] public ?string $keywordsPt = null; /** * @var X509Certificate[] */ - #[ORM\Column(name: 'certificates', type: SerializedArrayType::NAME, length: 65535)] - public $certificates = array(); + #[ORM\Column(name: 'certificates', type: CertificateArrayType::NAME, length: 65535, nullable: true)] + public array $certificates = array(); /** * @var string */ #[ORM\Column(name: 'workflow_state', type: Types::STRING)] - public ?string $workflowState = self::WORKFLOW_STATE_DEFAULT; + public string $workflowState = self::WORKFLOW_STATE_DEFAULT; /** * @var ContactPerson[] */ - #[ORM\Column(name: 'contact_persons', type: SerializedArrayType::NAME, length: 65535)] - public $contactPersons; + #[ORM\Column(name: 'contact_persons', type: ContactPersonArrayType::NAME, length: 65535, nullable: true)] + public array $contactPersons = array(); /** * @var string @@ -208,14 +212,14 @@ abstract class AbstractRole /** * @var string[] */ - #[ORM\Column(name: 'name_id_formats', type: SerializedArrayType::NAME, length: 65535)] - public $supportedNameIdFormats; + #[ORM\Column(name: 'name_id_formats', type: Types::JSON, length: 65535)] + public array $supportedNameIdFormats; /** * @var Service */ - #[ORM\Column(name: 'single_logout_service', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $singleLogoutService; + #[ORM\Column(name: 'single_logout_service', type: ServiceType::NAME, length: 65535, nullable: true)] + public ?Service $singleLogoutService; /** * @var bool @@ -226,20 +230,20 @@ abstract class AbstractRole /** * @var string */ - #[ORM\Column(name: 'manipulation', type: Types::TEXT, length: 65535)] + #[ORM\Column(name: 'manipulation', type: Types::TEXT, length: 65535, nullable: true)] public ?string $manipulation = null; /** * @var Coins */ #[ORM\Column(name: 'coins', type: 'engineblock_metadata_coins')] - protected $coins = array(); + protected Coins $coins; /** * @var Mdui */ #[ORM\Column(name: 'mdui', type: 'engineblock_metadata_mdui')] - protected $mdui; + protected Mdui $mdui; public function __construct( $entityId, @@ -250,19 +254,19 @@ public function __construct( ?Service $singleLogoutService = null, array $certificates = array(), array $contactPersons = array(), - ?string $descriptionEn = '', - ?string $descriptionNl = '', - ?string $descriptionPt = '', - ?string $displayNameEn = '', - ?string $displayNameNl = '', - ?string $displayNamePt = '', - ?string $keywordsEn = '', - ?string $keywordsNl = '', - ?string $keywordsPt = '', + ?string $descriptionEn = null, + ?string $descriptionNl = null, + ?string $descriptionPt = null, + ?string $displayNameEn = null, + ?string $displayNameNl = null, + ?string $displayNamePt = null, + ?string $keywordsEn = null, + ?string $keywordsNl = null, + ?string $keywordsPt = null, ?Logo $logo = null, - ?string $nameEn = '', - ?string $nameNl = '', - ?string $namePt = '', + ?string $nameEn = null, + ?string $nameNl = null, + ?string $namePt = null, ?string $nameIdFormat = null, array $supportedNameIdFormats = array( Constants::NAMEID_TRANSIENT, @@ -270,7 +274,7 @@ public function __construct( ), bool $requestsMustBeSigned = false, string $workflowState = self::WORKFLOW_STATE_DEFAULT, - string $manipulation = '' + ?string $manipulation = null ) { $this->mdui = $mdui; $this->certificates = $certificates; @@ -306,10 +310,7 @@ public function __construct( */ abstract public function accept(VisitorInterface $visitor); - /** - * @return string - */ - public function getManipulation() + public function getManipulation(): ?string { return $this->manipulation; } diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php new file mode 100644 index 000000000..448d9a767 --- /dev/null +++ b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php @@ -0,0 +1,347 @@ + ServiceProviderEb5::class, 'idp' => IdentityProviderEb5::class])] +#[ORM\Table(name: 'sso_provider_roles_eb5')] +#[ORM\Index(name: 'idx_sso_provider_roles_type', columns: ['type'])] +#[ORM\Index(name: 'idx_sso_provider_roles_entity_id', columns: ['entity_id'])] +#[ORM\UniqueConstraint(name: 'idx_sso_provider_roles_entity_id_type', columns: ['type', 'entity_id'])] +abstract class AbstractRoleEb5 +{ + const string TABLE_NAME = 'sso_provider_roles_eb5'; + const WORKFLOW_STATE_PROD = 'prodaccepted'; + const WORKFLOW_STATE_TEST = 'testaccepted'; + const WORKFLOW_STATE_DEFAULT = self::WORKFLOW_STATE_PROD; + + /** + * @var int + */ + #[ORM\Id] + #[ORM\Column(name: 'id', type: Types::INTEGER)] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + /** + * @var string + */ + #[ORM\Column(name: 'entity_id', type: Types::STRING)] + public ?string $entityId = null; + + /** + * @var string + */ + #[ORM\Column(name: 'name_nl', type: Types::STRING)] + public ?string $nameNl = null; + + /** + * @var string + */ + #[ORM\Column(name: 'name_en', type: Types::STRING)] + public ?string $nameEn = null; + + /** + * @var string + */ + #[ORM\Column(name: 'name_pt', type: Types::STRING)] + public ?string $namePt = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'description_nl', type: Types::STRING)] + public ?string $descriptionNl = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'description_en', type: Types::STRING)] + public ?string $descriptionEn = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'description_pt', type: Types::STRING)] + public ?string $descriptionPt = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'display_name_nl', type: Types::STRING)] + public ?string $displayNameNl = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'display_name_en', type: Types::STRING)] + public ?string $displayNameEn = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'display_name_pt', type: Types::STRING)] + public ?string $displayNamePt = null; + + /** + * @var Logo + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'logo', type: SerializedObjectType::NAME)] + public $logo; + + /** + * @var Organization + */ + #[ORM\Column(name: 'organization_nl_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] + public $organizationNl; + + /** + * @var Organization + */ + #[ORM\Column(name: 'organization_en_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] + public $organizationEn; + + /** + * @var Organization + */ + #[ORM\Column(name: 'organization_pt_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] + public $organizationPt; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'keywords_nl', type: Types::STRING)] + public ?string $keywordsNl = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'keywords_en', type: Types::STRING)] + public ?string $keywordsEn = null; + + /** + * @var string + * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead + */ + #[ORM\Column(name: 'keywords_pt', type: Types::STRING)] + public ?string $keywordsPt = null; + + /** + * @var X509Certificate[] + */ + #[ORM\Column(name: 'certificates', type: SerializedArrayType::NAME, length: 65535)] + public $certificates = array(); + + /** + * @var string + */ + #[ORM\Column(name: 'workflow_state', type: Types::STRING)] + public ?string $workflowState = self::WORKFLOW_STATE_DEFAULT; + + /** + * @var ContactPerson[] + */ + #[ORM\Column(name: 'contact_persons', type: SerializedArrayType::NAME, length: 65535)] + public $contactPersons; + + /** + * @var string + */ + #[ORM\Column(name: 'name_id_format', type: Types::STRING, nullable: true)] + public ?string $nameIdFormat = null; + + /** + * @var string[] + */ + #[ORM\Column(name: 'name_id_formats', type: SerializedArrayType::NAME, length: 65535)] + public $supportedNameIdFormats; + + /** + * @var Service + */ + #[ORM\Column(name: 'single_logout_service', type: SerializedObjectType::NAME, length: 65535, nullable: true)] + public $singleLogoutService; + + /** + * @var bool + */ + #[ORM\Column(name: 'requests_must_be_signed', type: Types::BOOLEAN)] + public ?bool $requestsMustBeSigned = false; + + /** + * @var string + */ + #[ORM\Column(name: 'manipulation', type: Types::TEXT, length: 65535)] + public ?string $manipulation = null; + + /** + * @var Coins + */ + #[ORM\Column(name: 'coins', type: 'engineblock_metadata_coins')] + protected $coins = array(); + + /** + * @var Mdui + */ + #[ORM\Column(name: 'mdui', type: 'engineblock_metadata_mdui')] + protected $mdui; + + public function __construct( + $entityId, + Mdui $mdui, + ?Organization $organizationEn = null, + ?Organization $organizationNl = null, + ?Organization $organizationPt = null, + ?Service $singleLogoutService = null, + array $certificates = array(), + array $contactPersons = array(), + ?string $descriptionEn = '', + ?string $descriptionNl = '', + ?string $descriptionPt = '', + ?string $displayNameEn = '', + ?string $displayNameNl = '', + ?string $displayNamePt = '', + ?string $keywordsEn = '', + ?string $keywordsNl = '', + ?string $keywordsPt = '', + ?Logo $logo = null, + ?string $nameEn = '', + ?string $nameNl = '', + ?string $namePt = '', + ?string $nameIdFormat = null, + array $supportedNameIdFormats = array( + Constants::NAMEID_TRANSIENT, + Constants::NAMEID_PERSISTENT, + ), + bool $requestsMustBeSigned = false, + string $workflowState = self::WORKFLOW_STATE_DEFAULT, + string $manipulation = '' + ) { + $this->mdui = $mdui; + $this->certificates = $certificates; + $this->contactPersons = $contactPersons; + $this->descriptionEn = $descriptionEn; + $this->descriptionNl = $descriptionNl; + $this->descriptionPt = $descriptionPt; + $this->displayNameEn = $displayNameEn; + $this->displayNameNl = $displayNameNl; + $this->displayNamePt = $displayNamePt; + $this->entityId = $entityId; + $this->keywordsEn = $keywordsEn; + $this->keywordsNl = $keywordsNl; + $this->keywordsPt = $keywordsPt; + $this->logo = $logo; + $this->nameEn = $nameEn; + $this->nameNl = $nameNl; + $this->namePt = $namePt; + $this->nameIdFormat = $nameIdFormat; + $this->supportedNameIdFormats = $supportedNameIdFormats; + $this->organizationEn = $organizationEn; + $this->organizationNl = $organizationNl; + $this->organizationPt = $organizationPt; + $this->requestsMustBeSigned = $requestsMustBeSigned; + $this->singleLogoutService = $singleLogoutService; + $this->workflowState = $workflowState; + $this->manipulation = $manipulation; + } + + /** + * @param VisitorInterface $visitor + * @return null|AbstractRole + */ + abstract public function accept(VisitorInterface $visitor); + + /** + * @return string + */ + public function getManipulation() + { + return $this->manipulation; + } + + /** + * @return $this + */ + public function toggleWorkflowState() + { + if ($this->workflowState === static::WORKFLOW_STATE_PROD) { + $this->workflowState = static::WORKFLOW_STATE_TEST; + return $this; + } + + if ($this->workflowState === static::WORKFLOW_STATE_TEST) { + $this->workflowState = static::WORKFLOW_STATE_PROD; + return $this; + } + + throw new RuntimeException('Unknown workflow state'); + } + + public function getCoins(): Coins + { + return $this->coins; + } + + public function getMdui(): Mdui + { + return $this->mdui; + } +} diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssembler.php b/src/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssembler.php index 6fddacfbb..6a5725266 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssembler.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssembler.php @@ -22,7 +22,9 @@ use OpenConext\EngineBlock\Metadata\ConsentSettings; use OpenConext\EngineBlock\Metadata\ContactPerson; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; +use OpenConext\EngineBlock\Metadata\Entity\IdentityProviderEb5; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlock\Metadata\Entity\ServiceProviderEb5; use OpenConext\EngineBlock\Metadata\Factory\MduiPushAssemblerFactory; use OpenConext\EngineBlock\Metadata\IndexedService; use OpenConext\EngineBlock\Metadata\Logo; @@ -663,4 +665,197 @@ private function validateManipulationCode(string $entityId, string $code): void ); } } + + /* + * TODO: Remove this code after sso_provider_roles_eb5 has been phased out + * @Deprecated + */ + public function assembleEb5($connections) + { + $roles = array(); + $allIdpEntityIds = array(); + $spAllowedEntityIds = array(); + $idpAllowedEntityIds = array(); + + foreach ($connections as $connection) { + $role = $this->assembleConnectionEb5($connection); + + if ($role instanceof ServiceProvider) { + if (isset($connection->allowed_connections)) { + $spAllowedEntityIds[$role->entityId] = array_map( + function ($allowedConnection) { + return $allowedConnection->name; + }, + $connection->allowed_connections + ); + } + + if (isset($connection->allow_all_entities) && $connection->allow_all_entities) { + $spAllowedEntityIds[$role->entityId] = true; + } + } + + if ($role instanceof IdentityProvider) { + $allIdpEntityIds[] = $role->entityId; + + if (isset($connection->allowed_connections)) { + $idpAllowedEntityIds[$role->entityId] = array_map( + function ($allowedConnection) { + return $allowedConnection->name; + }, + $connection->allowed_connections + ); + } + + if (isset($connection->allow_all_entities) && $connection->allow_all_entities) { + $idpAllowedEntityIds[$role->entityId] = true; + } + } + + $roles[] = $role; + } + + // For all service providers + foreach ($roles as $role) { + if (!$role instanceof ServiceProvider) { + continue; + } + + // Get the IdPs that are allowed for this SP. + $allowedIdpEntityIds = null; + if (isset($spAllowedEntityIds[$role->entityId])) { + $allowedIdpEntityIds = $spAllowedEntityIds[$role->entityId]; + if ($allowedIdpEntityIds === true) { + $allowedIdpEntityIds = $allIdpEntityIds; + } + } + + // Strip out the IdPs that disallow the SP + foreach ($idpAllowedEntityIds as $idpEntityId => $allowedSpEntityIds) { + if ($allowedSpEntityIds === true) { + continue; + } + + if (in_array($role->entityId, $allowedSpEntityIds)) { + continue; + } + + $index = array_search($idpEntityId, $allowedIdpEntityIds); + + if ($index === false) { + continue; + } + + + unset($allowedIdpEntityIds[$index]); + } + + if ($allowedIdpEntityIds === $allIdpEntityIds) { + // If a blacklist was configured, and no IDPs were explicitly + // blacklisted, then don't keep track of all entity IDs, but + // remember that all IDPs are allowed. + $role->allowAll = true; + } else { + $role->allowedIdpEntityIds = $allowedIdpEntityIds; + } + } + + if (count($roles) === 0) { + throw new RuntimeException('Received 0 connections, refusing to process'); + } + + return $roles; + } + + /* + * TODO: Remove this code after sso_provider_roles_eb5 has been phased out + * @Deprecated + */ + private function assembleConnectionEb5(stdClass $connection) + { + if ($connection->type === 'saml20-sp') { + return $this->assembleSpEb5($connection); + } + + if ($connection->type === 'saml20-idp') { + return $this->assembleIdpEb5($connection); + } + + throw new RuntimeException( + sprintf('Unrecognized type: "%s" "%s"', $connection->type, var_export($connection, true)) + ); + } + + /* + * TODO: Remove this code after sso_provider_roles_eb5 has been phased out + * @Deprecated + */ + private function assembleSpEb5(stdClass $connection) + { + $properties = $this->assembleCommon($connection); + + $properties += $this->assembleAttributeReleasePolicy($connection); + $properties += $this->assembleAssertionConsumerServices($connection); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:transparant_issuer'], 'isTransparentIssuer'); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:trusted_proxy'], 'isTrustedProxy'); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:display_unconnected_idps_wayf'], 'displayUnconnectedIdpsWayf'); + + $properties += $this->assembleIsConsentRequired($connection); + + $properties += $this->setPathFromObjectString([$connection, 'metadata:coin:eula'], 'termsOfServiceUrl'); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:do_not_add_attribute_aliases'], 'skipDenormalization'); + $properties += $this->setPathFromObjectBool( + [$connection, 'metadata:coin:policy_enforcement_decision_required'], + 'policyEnforcementDecisionRequired' + ); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:requesterid_required'], 'requesteridRequired'); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:sign_response'], 'signResponse'); + $properties += $this->setPathFromObjectString([$connection, 'metadata:coin:stepup:requireloa'], 'stepupRequireLoa'); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:stepup:allow_no_token'], 'stepupAllowNoToken'); + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:stepup:forceauthn'], 'stepupForceAuthn'); + + $properties += $this->setPathFromObjectBool([$connection, 'metadata:coin:collab_enabled'], 'collabEnabled'); + + return Utils::instantiate( + ServiceProviderEb5::class, + $properties + ); + } + + /* + * TODO: Remove this code after sso_provider_roles_eb5 has been phased out + * @Deprecated + */ + private function assembleIdpEb5(stdClass $connection) + { + $properties = $this->assembleCommon($connection); + + $properties += $this->assembleSingleSignOnServices($connection); + $properties += $this->setPathFromObjectString(array($connection, 'metadata:coin:guest_qualifier'), 'guestQualifier'); + $properties += $this->setPathFromObjectString(array($connection, 'metadata:coin:schachomeorganization'), 'schacHomeOrganization'); + $properties += $this->assembleConsentSettings($connection); + $properties += $this->setPathFromObjectBool(array($connection, 'metadata:coin:hidden'), 'hidden'); + $properties += $this->assembleShibMdScopes($connection); + + $properties += $this->assembleStepupConnections($connection); + $properties += $this->assembleMfaEntities($connection); + + $properties += $this->discoveryAssembler->assembleDiscoveries($connection); + $properties += $this->setPathFromObjectString(array($connection, 'metadata:coin:defaultRAC'), 'defaultRAC'); + + $properties += $this->setPathFromObjectBool( + [$connection, 'metadata:coin:policy_enforcement_decision_required'], + 'policyEnforcementDecisionRequired' + ); + + $properties += $this->setPathFromObjectString( + [$connection, 'metadata:coin:azure_domain_hint'], + 'azureDomainHint' + ); + + return Utils::instantiate( + IdentityProviderEb5::class, + $properties + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProvider.php b/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProvider.php index 75d1846f4..188e66015 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProvider.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProvider.php @@ -34,7 +34,8 @@ use OpenConext\EngineBlock\Metadata\ShibMdScope; use OpenConext\EngineBlock\Metadata\StepupConnections; use OpenConext\EngineBlockBundle\Doctrine\Type\LegacyJsonType; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\ServiceArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\ShibMdScopeArrayType; use RobRichards\XMLSecLibs\XMLSecurityKey; use SAML2\Constants; @@ -68,13 +69,13 @@ class IdentityProvider extends AbstractRole /** * @var bool */ - #[ORM\Column(name: 'enabled_in_wayf', type: Types::BOOLEAN)] + #[ORM\Column(name: 'enabled_in_wayf', type: Types::BOOLEAN, nullable: true)] public ?bool $enabledInWayf = true; /** * @var Service[] */ - #[ORM\Column(name: 'single_sign_on_services', type: SerializedArrayType::NAME, length: 65535)] + #[ORM\Column(name: 'single_sign_on_services', type: ServiceArrayType::NAME, length: 65535, nullable: true)] public $singleSignOnServices = array(); /** @@ -91,19 +92,19 @@ class IdentityProvider extends AbstractRole * with green/blue deployment strategies. * */ - #[ORM\Column(name: 'consent_settings', type: LegacyJsonType::NAME)] + #[ORM\Column(name: 'consent_settings', type: LegacyJsonType::NAME, nullable: true)] private $consentSettings; /** * @var ShibMdScope[] */ - #[ORM\Column(name: 'shib_md_scopes', type: SerializedArrayType::NAME, length: 65535)] + #[ORM\Column(name: 'shib_md_scopes', type: ShibMdScopeArrayType::NAME, length: 65535, nullable: true)] public $shibMdScopes = array(); /** * @var array */ - #[ORM\Column(name: 'idp_discoveries', type: LegacyJsonType::NAME)] + #[ORM\Column(name: 'idp_discoveries', type: LegacyJsonType::NAME, nullable: true)] private $discoveries; /** @@ -121,20 +122,20 @@ public function __construct( bool $additionalLogging = false, array $certificates = array(), array $contactPersons = array(), - string $descriptionEn = '', - string $descriptionNl = '', - string $descriptionPt = '', + ?string $descriptionEn = null, + ?string $descriptionNl = null, + ?string $descriptionPt = null, bool $disableScoping = false, - string $displayNameEn = '', - string $displayNameNl = '', - string $displayNamePt = '', - string $keywordsEn = '', - string $keywordsNl = '', - string $keywordsPt = '', + ?string $displayNameEn = null, + ?string $displayNameNl = null, + ?string $displayNamePt = null, + ?string $keywordsEn = null, + ?string $keywordsNl = null, + ?string $keywordsPt = null, ?Logo $logo = null, - string $nameEn = '', - string $nameNl = '', - string $namePt = '', + ?string $nameEn = null, + ?string $nameNl = null, + ?string $namePt = null, ?string $nameIdFormat = null, array $supportedNameIdFormats = array( Constants::NAMEID_TRANSIENT, @@ -143,7 +144,7 @@ public function __construct( bool $requestsMustBeSigned = false, string $signatureMethod = XMLSecurityKey::RSA_SHA256, string $workflowState = self::WORKFLOW_STATE_DEFAULT, - string $manipulation = '', + ?string $manipulation = null, bool $enabledInWayf = true, string $guestQualifier = self::GUEST_QUALIFIER_ALL, bool $hidden = false, diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php b/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php new file mode 100644 index 000000000..934b82a0b --- /dev/null +++ b/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php @@ -0,0 +1,371 @@ + + */ + #[ORM\Column(name: 'idp_discoveries', type: LegacyJsonType::NAME)] + private $discoveries; + + /** + * WARNING: Please don't use this entity directly but use the dedicated factory instead. + * @see \OpenConext\EngineBlock\Metadata\Factory\Factory\IdentityProviderFactory + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function __construct( + $entityId, + ?Mdui $mdui = null, + ?Organization $organizationEn = null, + ?Organization $organizationNl = null, + ?Organization $organizationPt = null, + ?Service $singleLogoutService = null, + bool $additionalLogging = false, + array $certificates = array(), + array $contactPersons = array(), + string $descriptionEn = '', + string $descriptionNl = '', + string $descriptionPt = '', + bool $disableScoping = false, + string $displayNameEn = '', + string $displayNameNl = '', + string $displayNamePt = '', + string $keywordsEn = '', + string $keywordsNl = '', + string $keywordsPt = '', + ?Logo $logo = null, + string $nameEn = '', + string $nameNl = '', + string $namePt = '', + ?string $nameIdFormat = null, + array $supportedNameIdFormats = array( + Constants::NAMEID_TRANSIENT, + Constants::NAMEID_PERSISTENT, + ), + bool $requestsMustBeSigned = false, + string $signatureMethod = XMLSecurityKey::RSA_SHA256, + string $workflowState = self::WORKFLOW_STATE_DEFAULT, + string $manipulation = '', + bool $enabledInWayf = true, + string $guestQualifier = self::GUEST_QUALIFIER_ALL, + bool $hidden = false, + ?string $schacHomeOrganization = null, + array $shibMdScopes = array(), + array $singleSignOnServices = array(), + ?ConsentSettings $consentSettings = null, + ?StepupConnections $stepupConnections = null, + ?MfaEntityCollection $mfaEntities = null, + array $discoveries = [], + ?string $defaultRAC = null, + bool $policyEnforcementDecisionRequired = false, + ?string $azureDomainHint = null + ) { + if (is_null($mdui)) { + $mdui = Mdui::emptyMdui(); + } + parent::__construct( + $entityId, + $mdui, + $organizationEn, + $organizationNl, + $organizationPt, + $singleLogoutService, + $certificates, + $contactPersons, + $descriptionEn, + $descriptionNl, + $descriptionPt, + $displayNameEn, + $displayNameNl, + $displayNamePt, + $keywordsEn, + $keywordsNl, + $keywordsPt, + $logo, + $nameEn, + $nameNl, + $namePt, + $nameIdFormat, + $supportedNameIdFormats, + $requestsMustBeSigned, + $workflowState, + $manipulation + ); + + $this->enabledInWayf = $enabledInWayf; + $this->shibMdScopes = $shibMdScopes; + $this->singleSignOnServices = $singleSignOnServices; + $this->consentSettings = $consentSettings; + + $this->coins = Coins::createForIdentityProvider( + $guestQualifier, + $schacHomeOrganization, + $hidden, + $stepupConnections, + $disableScoping, + $additionalLogging, + $signatureMethod, + $mfaEntities, + $defaultRAC, + $policyEnforcementDecisionRequired, + $azureDomainHint + ); + + $this->assertAllDiscoveries($discoveries); + $this->discoveries = $discoveries; + } + + /** + * {@inheritdoc} + */ + public function accept(VisitorInterface $visitor) + { + $visitor->visitIdentityProvider($this); + } + + /** + * @param string $preferredLocale + * @return string + */ + public function getDisplayName($preferredLocale = '') + { + $idpName = ''; + if ($preferredLocale === 'nl') { + $idpName = $this->nameNl; + } elseif ($preferredLocale === 'en') { + $idpName = $this->nameEn; + } elseif ($preferredLocale === 'pt') { + $idpName = $this->namePt; + } + if (empty($idpName)) { + $idpName = $this->entityId; + } + return $idpName; + } + + /** + * @param ConsentSettings $settings + * @return IdentityProvider + */ + public function setConsentSettings(ConsentSettings $settings) + { + $this->consentSettings = $settings; + + return $this; + } + + /** + * @return ConsentSettings + */ + public function getConsentSettings() + { + if (!$this->consentSettings instanceof ConsentSettings) { + $this->setConsentSettings( + new ConsentSettings( + (array)$this->consentSettings + ) + ); + } + + return $this->consentSettings; + } + + /** + * @return array + */ + public function getDiscoveries(): array + { + $this->ensureDiscoveriesDeserialized(); + return $this->discoveries; + } + + /** + * @param array $discoveries + */ + public function setDiscoveries(array $discoveries) + { + $this->assertAllDiscoveries($discoveries); + $this->discoveries = $discoveries; + } + + private function ensureDiscoveriesDeserialized(): void + { + if (!is_array($this->discoveries)) { + $this->discoveries = []; + return; + } + + foreach ($this->discoveries as $index => $discovery) { + try { + if (!$discovery instanceof Discovery) { + $logo = null; + if (isset($discovery['logo']) && is_array($discovery['logo'])) { + $logo = new Logo($discovery['logo']['url']); + $logo->width = $discovery['logo']['width']; + $logo->height = $discovery['logo']['height']; + } + + $this->discoveries[$index] = Discovery::create( + $discovery['names'] ?? [], + $discovery['keywords'] ?? [], + $logo + ); + } + } catch (InvalidDiscoveryException $e) { + unset($this->discoveries[$index]); + } + } + } + + private function assertAllDiscoveries(array $discoveries): void + { + foreach ($discoveries as $discovery) { + if (!$discovery instanceof Discovery) { + throw new InvalidArgumentException('Discovery must be instance of Discovery'); + } + } + } + + /** + * Certificates are not available on the object after deserialisation! + * + * @return array + */ + public function __serialize(): array + { + return [ + 'enabledInWayf' => $this->enabledInWayf, + 'singleSignOnServices' => $this->singleSignOnServices, + 'consentSettings' => $this->consentSettings, + 'shibMdScopes' => $this->shibMdScopes, + 'discoveries' => $this->discoveries, + 'id' => $this->id, + 'entityId' => $this->entityId, + 'nameNl' => $this->nameNl, + 'nameEn' => $this->nameEn, + 'namePt' => $this->namePt, + 'descriptionNl' => $this->descriptionNl, + 'descriptionEn' => $this->descriptionEn, + 'descriptionPt' => $this->descriptionPt, + 'displayNameNl' => $this->displayNameNl, + 'displayNameEn' => $this->displayNameEn, + 'displayNamePt' => $this->displayNamePt, + 'logo' => $this->logo, + 'organizationNl' => $this->organizationNl, + 'organizationEn' => $this->organizationEn, + 'organizationPt' => $this->organizationPt, + 'keywordsNl' => $this->keywordsNl, + 'keywordsEn' => $this->keywordsEn, + 'keywordsPt' => $this->keywordsPt, + 'workflowState' => $this->workflowState, + 'contactPersons' => $this->contactPersons, + 'nameIdFormat' => $this->nameIdFormat, + 'supportedNameIdFormats' => $this->supportedNameIdFormats, + 'singleLogoutService' => $this->singleLogoutService, + 'requestsMustBeSigned' => $this->requestsMustBeSigned, + 'manipulation' => $this->manipulation, + 'coins' => $this->coins, + 'mdui' => $this->mdui, + ]; + } +} diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProvider.php b/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProvider.php index 7d81229c2..5b994e71b 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProvider.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProvider.php @@ -30,7 +30,9 @@ use OpenConext\EngineBlock\Metadata\Organization; use OpenConext\EngineBlock\Metadata\RequestedAttribute; use OpenConext\EngineBlock\Metadata\Service; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\AttributeReleasePolicyType; +use OpenConext\EngineBlockBundle\Doctrine\Type\IndexedServiceArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\RequestedAttributeArrayType; use RobRichards\XMLSecLibs\XMLSecurityKey; use SAML2\Constants; @@ -50,31 +52,31 @@ class ServiceProvider extends AbstractRole /** * @var null|AttributeReleasePolicy */ - #[ORM\Column(name: 'attribute_release_policy', type: SerializedArrayType::NAME, length: 65535)] + #[ORM\Column(name: 'attribute_release_policy', type: AttributeReleasePolicyType::NAME, length: 65535, nullable: true)] public $attributeReleasePolicy; /** * @var IndexedService[] */ - #[ORM\Column(name: 'assertion_consumer_services', type: SerializedArrayType::NAME, length: 65535)] + #[ORM\Column(name: 'assertion_consumer_services', type: IndexedServiceArrayType::NAME, length: 65535, nullable: true)] public $assertionConsumerServices; /** * @var string[] */ - #[ORM\Column(name: 'allowed_idp_entity_ids', type: SerializedArrayType::NAME, length: 6777215)] + #[ORM\Column(name: 'allowed_idp_entity_ids', type: Types::JSON, length: 6777215, nullable: true)] public $allowedIdpEntityIds; /** * @var bool */ - #[ORM\Column(name: 'allow_all', type: Types::BOOLEAN)] + #[ORM\Column(name: 'allow_all', type: Types::BOOLEAN, nullable: true)] public ?bool $allowAll = null; /** * @var null|RequestedAttribute[] */ - #[ORM\Column(name: 'requested_attributes', type: SerializedArrayType::NAME, length: 65535)] + #[ORM\Column(name: 'requested_attributes', type: RequestedAttributeArrayType::NAME, length: 65535, nullable: true)] public $requestedAttributes; /** @@ -109,20 +111,20 @@ public function __construct( bool $additionalLogging = false, array $certificates = array(), array $contactPersons = array(), - ?string $descriptionEn = '', - ?string $descriptionNl = '', - ?string $descriptionPt = '', + ?string $descriptionEn = null, + ?string $descriptionNl = null, + ?string $descriptionPt = null, bool $disableScoping = false, - ?string $displayNameEn = '', - ?string $displayNameNl = '', - ?string $displayNamePt = '', - ?string $keywordsEn = '', - ?string $keywordsNl = '', - ?string $keywordsPt = '', + ?string $displayNameEn = null, + ?string $displayNameNl = null, + ?string $displayNamePt = null, + ?string $keywordsEn = null, + ?string $keywordsNl = null, + ?string $keywordsPt = null, ?Logo $logo = null, - ?string $nameEn = '', - ?string $nameNl = '', - ?string $namePt = '', + ?string $nameEn = null, + ?string $nameNl = null, + ?string $namePt = null, ?string $nameIdFormat = null, array $supportedNameIdFormats = array( Constants::NAMEID_TRANSIENT, @@ -144,7 +146,7 @@ public function __construct( bool $policyEnforcementDecisionRequired = false, bool $requesteridRequired = false, bool $signResponse = false, - string $manipulation = '', + ?string $manipulation = null, ?AttributeReleasePolicy $attributeReleasePolicy = null, ?string $supportUrlEn = null, ?string $supportUrlNl = null, diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php b/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php new file mode 100644 index 000000000..eb104fb9f --- /dev/null +++ b/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php @@ -0,0 +1,416 @@ +attributeReleasePolicy = $attributeReleasePolicy; + $this->allowedIdpEntityIds = $allowedIdpEntityIds; + $this->allowAll = $allowAll; + $this->assertionConsumerServices = $assertionConsumerServices; + $this->requestedAttributes = $requestedAttributes; + $this->supportUrlEn = $supportUrlEn; + $this->supportUrlNl = $supportUrlNl; + $this->supportUrlPt = $supportUrlPt; + + $this->coins = Coins::createForServiceProvider( + $isConsentRequired, + $isTransparentIssuer, + $isTrustedProxy, + $displayUnconnectedIdpsWayf, + $termsOfServiceUrl, + $skipDenormalization, + $policyEnforcementDecisionRequired, + $requesteridRequired, + $signResponse, + $stepupAllowNoToken, + $stepupRequireLoa, + $disableScoping, + $additionalLogging, + $signatureMethod, + $stepupForceAuthn, + $collabEnabled + ); + } + + /** + * This is a factory method to convert the immutable ServiceProviderEntityInterface to the legacy domain entity. + * + * @param ServiceProviderEntityInterface $serviceProvider + * @return ServiceProvider + */ + public static function fromServiceProviderEntity(ServiceProviderEntityInterface $serviceProvider): ServiceProviderEb5 + { + $entity = new self($serviceProvider->getEntityId(), $serviceProvider->getMdui()); + $entity->id = $serviceProvider->getId(); + $entity->entityId = $serviceProvider->getEntityId(); + $entity->nameNl = $serviceProvider->getName('nl'); + $entity->nameEn = $serviceProvider->getName('en'); + $entity->namePt = $serviceProvider->getName('pt'); + $entity->descriptionNl = $serviceProvider->getDescription('nl'); + $entity->descriptionEn = $serviceProvider->getDescription('en'); + $entity->descriptionPt = $serviceProvider->getDescription('pt'); + $entity->displayNameNl = $serviceProvider->getDisplayName('nl'); + $entity->displayNameEn = $serviceProvider->getDisplayName('en'); + $entity->displayNamePt = $serviceProvider->getDisplayName('pt'); + $entity->getMdui()->setLogo($serviceProvider->getLogo()); + + $entity->organizationNl = $serviceProvider->getOrganization('nl'); + $entity->organizationEn = $serviceProvider->getOrganization('en'); + $entity->organizationPt = $serviceProvider->getOrganization('pt'); + $entity->keywordsNl = $serviceProvider->getKeywords('nl'); + $entity->keywordsEn = $serviceProvider->getKeywords('en'); + $entity->keywordsPt = $serviceProvider->getKeywords('pt'); + $entity->certificates = $serviceProvider->getCertificates(); + $entity->workflowState = $serviceProvider->getWorkflowState(); + $entity->contactPersons = $serviceProvider->getContactPersons(); + $entity->nameIdFormat = $serviceProvider->getNameIdFormat(); + $entity->supportedNameIdFormats = $serviceProvider->getSupportedNameIdFormats(); + $entity->singleLogoutService = $serviceProvider->getSingleLogoutService(); + $entity->requestsMustBeSigned = $serviceProvider->isRequestsMustBeSigned(); + $entity->manipulation = $serviceProvider->getManipulation(); + $entity->coins = $serviceProvider->getCoins(); + $entity->attributeReleasePolicy = $serviceProvider->getAttributeReleasePolicy(); + $entity->assertionConsumerServices = $serviceProvider->getAssertionConsumerServices(); + $entity->allowedIdpEntityIds = $serviceProvider->getAllowedIdpEntityIds(); + $entity->allowAll = $serviceProvider->isAllowAll(); + $entity->requestedAttributes = $serviceProvider->getRequestedAttributes(); + $entity->supportUrlNl = $serviceProvider->getSupportUrl('nl'); + $entity->supportUrlEn = $serviceProvider->getSupportUrl('en'); + $entity->supportUrlPt = $serviceProvider->getSupportUrl('pt'); + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function accept(VisitorInterface $visitor) + { + $visitor->visitServiceProvider($this); + } + + /** + * @return null|AttributeReleasePolicy + */ + public function getAttributeReleasePolicy() + { + return $this->attributeReleasePolicy; + } + + /** + * @param string $idpEntityId + * @return bool + */ + public function isAllowed($idpEntityId) + { + return $this->allowAll || in_array($idpEntityId, $this->allowedIdpEntityIds); + } + + /** + * Algorithm for display name is: + * 1. Display name in preferred locale + * 2. Name in preferred locale + * 3. Display name in English + * 4. Name in English + * 5. EntityID (should never happen) + */ + public function getDisplayName(string $preferredLocale = 'en'): string + { + + $preferredName = $this->mdui->getDisplayName($preferredLocale); + $fallback = 'name' . ucfirst($preferredLocale); + + if ($preferredName !== '') { + $spName = $preferredName; + } elseif (isset($this->$fallback)) { + $spName = $this->$fallback; + } + + if ($preferredLocale !== 'en' & empty($spName)) { + $englishDisplayName = $this->mdui->getDisplayName('en'); + $spName = !empty($englishDisplayName) ? $englishDisplayName : $this->nameEn; + } + + if (empty($spName)) { + $spName = $this->entityId; + } + + return $spName; + } + + /** + * Algorithm for organization name is + * 1. Organization display name in preferred locale + * 2. Organization name in preferred locale + * 3. English organization display name + * 4. English organization name + * 5. Empty string (will be set to the locale-specific variant of 'unknown' in the template) + */ + public function getOrganizationName(string $preferredLocale = 'en'): string + { + $orgLocale = 'organization' . ucfirst($preferredLocale); + // Load the preferred locale org. display name, falling back on org. name + if (isset($this->$orgLocale)) { + $orgName = !empty($this->$orgLocale->displayName) + ? $this->$orgLocale->displayName + : $this->$orgLocale->name; + } + + // Fallback to EN naming preferences when the preferred locale was not set or yielded no value + if ((($preferredLocale !== 'en' && empty($orgName)) || empty($orgName)) && isset($this->organizationEn)) { + $orgName = !empty($this->organizationEn->displayName) ? $this->organizationEn->displayName : $this->organizationEn->name; + } + + // Show empty string when no translation was found (virtually impossible) + if (empty($orgName)) { + $orgName = ''; + } + + return $orgName; + } + + /** + * @return bool + */ + public function isAttributeAggregationRequired() + { + if (is_null($this->attributeReleasePolicy)) { + return false; + } + + $rules = $this->attributeReleasePolicy->getRulesWithSourceSpecification(); + + return count($rules) > 0; + } + + /** + * Certificates are not available on the object after deserialisation! + * + * @return array + */ + public function __serialize(): array + { + return [ + 'attributeReleasePolicy' => $this->attributeReleasePolicy, + 'assertionConsumerServices' => $this->assertionConsumerServices, + 'allowedIdpEntityIds' => $this->allowedIdpEntityIds, + 'allowAll' => $this->allowAll, + 'requestedAttributes' => $this->requestedAttributes, + 'supportUrlEn' => $this->supportUrlEn, + 'supportUrlNl' => $this->supportUrlNl, + 'supportUrlPt' => $this->supportUrlPt, + 'id' => $this->id, + 'entityId' => $this->entityId, + 'nameNl' => $this->nameNl, + 'nameEn' => $this->nameEn, + 'namePt' => $this->namePt, + 'descriptionNl' => $this->descriptionNl, + 'descriptionEn' => $this->descriptionEn, + 'descriptionPt' => $this->descriptionPt, + 'displayNameNl' => $this->displayNameNl, + 'displayNameEn' => $this->displayNameEn, + 'displayNamePt' => $this->displayNamePt, + 'logo' => $this->logo, + 'organizationNl' => $this->organizationNl, + 'organizationEn' => $this->organizationEn, + 'organizationPt' => $this->organizationPt, + 'keywordsNl' => $this->keywordsNl, + 'keywordsEn' => $this->keywordsEn, + 'keywordsPt' => $this->keywordsPt, + 'workflowState' => $this->workflowState, + 'contactPersons' => $this->contactPersons, + 'nameIdFormat' => $this->nameIdFormat, + 'supportedNameIdFormats' => $this->supportedNameIdFormats, + 'singleLogoutService' => $this->singleLogoutService, + 'requestsMustBeSigned' => $this->requestsMustBeSigned, + 'manipulation' => $this->manipulation, + 'coins' => $this->coins, + 'mdui' => $this->mdui, + ]; + } +} diff --git a/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/IdentityProviderEntity.php b/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/IdentityProviderEntity.php index c1acb6f9c..f7666e67a 100644 --- a/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/IdentityProviderEntity.php +++ b/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/IdentityProviderEntity.php @@ -37,37 +37,24 @@ */ class IdentityProviderEntity implements IdentityProviderEntityInterface { - /** - * @var IdentityProvider - */ - private $entity; + private IdentityProvider $entity; public function __construct(IdentityProvider $entity) { $this->entity = $entity; } - /** - * @return int - */ public function getId(): int { return $this->entity->id; } - /** - * @return string - */ public function getEntityId(): string { return $this->entity->entityId; } - /** - * @param $locale - * @return string - */ - public function getName($locale): string + public function getName(string $locale): ?string { switch (true) { case ($locale == 'nl'): @@ -81,11 +68,7 @@ public function getName($locale): string return ''; } - /** - * @param $locale - * @return string - */ - public function getDescription($locale): string + public function getDescription(string $locale): ?string { if ($this->entity->getMdui()->hasDescription($locale)) { return $this->entity->getMdui()->getDescription($locale); @@ -94,11 +77,7 @@ public function getDescription($locale): string return ''; } - /** - * @param $locale - * @return string - */ - public function getDisplayName($locale): string + public function getDisplayName(string $locale): string { if ($this->entity->getMdui()->hasDisplayName($locale)) { return $this->entity->getMdui()->getDisplayName($locale); @@ -125,10 +104,6 @@ public function hasCompleteOrganizationData(string $locale): bool return false; } - /** - * @param string $locale - * @return Organization - */ public function getOrganization(string $locale): ?Organization { switch (true) { @@ -143,7 +118,7 @@ public function getOrganization(string $locale): ?Organization return null; } - public function getKeywords($locale): string + public function getKeywords(string $locale): ?string { if ($this->entity->getMdui()->hasKeywords($locale)) { return $this->entity->getMdui()->getKeywords($locale); @@ -160,9 +135,6 @@ public function getCertificates(): array return $this->entity->certificates; } - /** - * @return string - */ public function getWorkflowState(): string { return $this->entity->workflowState; @@ -176,9 +148,6 @@ public function getContactPersons(): array return $this->entity->contactPersons; } - /** - * @return string - */ public function getNameIdFormat(): string { return $this->entity->nameIdFormat; @@ -192,41 +161,26 @@ public function getSupportedNameIdFormats(): array return $this->entity->supportedNameIdFormats; } - /** - * @return Service|null - */ public function getSingleLogoutService(): ?Service { return $this->entity->singleLogoutService; } - /** - * @return bool - */ public function isRequestsMustBeSigned(): bool { return $this->entity->requestsMustBeSigned; } - /** - * @return string - */ - public function getManipulation(): string + public function getManipulation(): ?string { return $this->entity->manipulation; } - /** - * @return Coins - */ public function getCoins(): Coins { return $this->entity->getCoins(); } - /** - * @return bool - */ public function isEnabledInWayf(): bool { return $this->entity->enabledInWayf; @@ -240,9 +194,6 @@ public function getSingleSignOnServices(): array return $this->entity->singleSignOnServices; } - /** - * @return ConsentSettings - */ public function getConsentSettings(): ConsentSettings { return $this->entity->getConsentSettings(); diff --git a/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/ServiceProviderEntity.php b/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/ServiceProviderEntity.php index f6def4004..25238691c 100644 --- a/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/ServiceProviderEntity.php +++ b/src/OpenConext/EngineBlock/Metadata/Factory/Adapter/ServiceProviderEntity.php @@ -40,37 +40,24 @@ */ class ServiceProviderEntity implements ServiceProviderEntityInterface { - /** - * @var ServiceProvider - */ - private $entity; + private ServiceProvider $entity; public function __construct(ServiceProvider $entity) { $this->entity = $entity; } - /** - * @return null|int - */ public function getId(): ?int { return $this->entity->id; } - /** - * @return string - */ public function getEntityId(): string { return $this->entity->entityId; } - /** - * @param $locale - * @return string - */ - public function getName($locale): string + public function getName(string $locale): ?string { switch (true) { case ($locale == 'nl'): @@ -84,11 +71,7 @@ public function getName($locale): string return ''; } - /** - * @param $locale - * @return string - */ - public function getDescription($locale): string + public function getDescription(string $locale): string { if ($this->entity->getMdui()->hasDescription($locale)) { return $this->entity->getMdui()->getDescription($locale); @@ -106,9 +89,6 @@ public function getDisplayName(string $locale): string return ''; } - /** - * @return Logo - */ public function getLogo(): ?Logo { return $this->entity->getMdui()->getLogoOrNull(); @@ -127,10 +107,6 @@ public function hasCompleteOrganizationData(string $locale): bool return false; } - /** - * @param string $locale - * @return Organization - */ public function getOrganization(string $locale): ?Organization { switch (true) { @@ -145,11 +121,7 @@ public function getOrganization(string $locale): ?Organization return null; } - /** - * @param $locale - * @return string - */ - public function getKeywords($locale): string + public function getKeywords(string $locale): string { if ($this->entity->getMdui()->hasKeywords($locale)) { return $this->entity->getMdui()->getKeywords($locale); @@ -166,9 +138,6 @@ public function getCertificates(): array return $this->entity->certificates; } - /** - * @return string - */ public function getWorkflowState(): string { return $this->entity->workflowState; @@ -182,9 +151,6 @@ public function getContactPersons(): array return $this->entity->contactPersons; } - /** - * @return null|string - */ public function getNameIdFormat(): ?string { return $this->entity->nameIdFormat; @@ -198,33 +164,21 @@ public function getSupportedNameIdFormats(): array return $this->entity->supportedNameIdFormats; } - /** - * @return null|Service - */ public function getSingleLogoutService(): ?Service { return $this->entity->singleLogoutService; } - /** - * @return bool - */ public function isRequestsMustBeSigned(): bool { return $this->entity->requestsMustBeSigned; } - /** - * @return string - */ - public function getManipulation(): string + public function getManipulation(): ?string { return $this->entity->manipulation; } - /** - * @return Coins - */ public function getCoins(): Coins { return $this->entity->getCoins(); @@ -235,9 +189,6 @@ public function getMdui(): Mdui return $this->entity->getMdui(); } - /** - * @return AttributeReleasePolicy|null - */ public function getAttributeReleasePolicy(): ?AttributeReleasePolicy { return $this->entity->attributeReleasePolicy; @@ -259,9 +210,6 @@ public function getAllowedIdpEntityIds(): array return $this->entity->allowedIdpEntityIds; } - /** - * @return bool - */ public function isAllowAll(): bool { return $this->entity->allowAll; @@ -275,11 +223,7 @@ public function getRequestedAttributes(): ?array return $this->entity->requestedAttributes; } - /** - * @param $locale - * @return string|null - */ - public function getSupportUrl($locale): ?string + public function getSupportUrl(string $locale): ?string { switch (true) { case ($locale == 'nl'): @@ -293,18 +237,11 @@ public function getSupportUrl($locale): ?string return ''; } - /** - * @param string $idpEntityId - * @return bool - */ public function isAllowed(string $idpEntityId): bool { return $this->entity->isAllowed($idpEntityId); } - /** - * @return bool - */ public function isAttributeAggregationRequired(): bool { return $this->entity->isAttributeAggregationRequired(); diff --git a/src/OpenConext/EngineBlock/Metadata/Factory/Decorator/AbstractServiceProvider.php b/src/OpenConext/EngineBlock/Metadata/Factory/Decorator/AbstractServiceProvider.php index ae9d5ef91..0b491d5a5 100644 --- a/src/OpenConext/EngineBlock/Metadata/Factory/Decorator/AbstractServiceProvider.php +++ b/src/OpenConext/EngineBlock/Metadata/Factory/Decorator/AbstractServiceProvider.php @@ -37,57 +37,38 @@ abstract class AbstractServiceProvider implements ServiceProviderEntityInterface { - /** - * @var ServiceProviderEntityInterface - */ - protected $entity; + protected ServiceProviderEntityInterface $entity; - /** - * @param ServiceProviderEntityInterface $entity - */ public function __construct(ServiceProviderEntityInterface $entity) { $this->entity = $entity; } - /** - * @return null|int - */ public function getId(): ?int { return $this->entity->getId(); } - /** - * @return string - */ public function getEntityId(): string { return $this->entity->getEntityId(); } - /** - * @param $locale - * @return string - */ - public function getName($locale): string + public function getName(string $locale): ?string { return $this->entity->getName($locale); } - public function getDescription(string $locale): string + public function getDescription(string $locale): ?string { return $this->entity->getDescription($locale); } - public function getDisplayName(string $locale): string + public function getDisplayName(string $locale): ?string { return $this->entity->getDisplayName($locale); } - /** - * @return Logo|null - */ public function getLogo(): ?Logo { return $this->entity->getLogo(); @@ -106,20 +87,12 @@ public function hasCompleteOrganizationData(string $locale): bool return false; } - /** - * @param string $locale - * @return Organization|null - */ public function getOrganization(string $locale): ?Organization { return $this->entity->getOrganization($locale); } - /** - * @param $locale - * @return string - */ - public function getKeywords($locale): string + public function getKeywords(string $locale): string { return $this->entity->getKeywords($locale); } @@ -132,9 +105,6 @@ public function getCertificates(): array return $this->entity->getCertificates(); } - /** - * @return string - */ public function getWorkflowState(): string { return $this->entity->getWorkflowState(); @@ -148,9 +118,6 @@ public function getContactPersons(): array return $this->entity->getContactPersons(); } - /** - * @return null|string - */ public function getNameIdFormat(): ?string { return $this->entity->getNameIdFormat(); @@ -164,41 +131,26 @@ public function getSupportedNameIdFormats(): array return $this->entity->getSupportedNameIdFormats(); } - /** - * @return null|Service - */ public function getSingleLogoutService(): ?Service { return $this->entity->getSingleLogoutService(); } - /** - * @return bool - */ public function isRequestsMustBeSigned(): bool { return $this->entity->isRequestsMustBeSigned(); } - /** - * @return string - */ - public function getManipulation(): string + public function getManipulation(): ?string { return $this->entity->getManipulation(); } - /** - * @return Coins - */ public function getCoins(): Coins { return $this->entity->getCoins(); } - /** - * @return AttributeReleasePolicy|null - */ public function getAttributeReleasePolicy(): ?AttributeReleasePolicy { return $this->entity->getAttributeReleasePolicy(); @@ -220,9 +172,6 @@ public function getAllowedIdpEntityIds(): array return $this->entity->getAllowedIdpEntityIds(); } - /** - * @return bool - */ public function isAllowAll(): bool { return $this->entity->isAllowAll(); @@ -236,26 +185,16 @@ public function getRequestedAttributes(): ?array return $this->entity->getRequestedAttributes(); } - /** - * @return string|null - */ public function getSupportUrl($locale): ?string { return $this->entity->getSupportUrl($locale); } - /** - * @param string $idpEntityId - * @return bool - */ public function isAllowed(string $idpEntityId): bool { return $this->entity->isAllowed($idpEntityId); } - /** - * @return bool - */ public function isAttributeAggregationRequired(): bool { return $this->entity->isAttributeAggregationRequired(); diff --git a/src/OpenConext/EngineBlock/Metadata/Factory/IdentityProviderEntityInterface.php b/src/OpenConext/EngineBlock/Metadata/Factory/IdentityProviderEntityInterface.php index ec4865748..da31caf88 100644 --- a/src/OpenConext/EngineBlock/Metadata/Factory/IdentityProviderEntityInterface.php +++ b/src/OpenConext/EngineBlock/Metadata/Factory/IdentityProviderEntityInterface.php @@ -44,19 +44,19 @@ public function getEntityId(): string; * @param $locale * @return string */ - public function getName($locale): string; + public function getName(string $locale): ?string; /** * @param $locale * @return string */ - public function getDescription($locale): string; + public function getDescription(string $locale): ?string; /** * @param $locale * @return string */ - public function getDisplayName($locale): string; + public function getDisplayName(string $locale): ?string; /** * @return Logo|null @@ -79,7 +79,7 @@ public function getOrganization(string $locale): ?Organization; * @param $locale * @return string */ - public function getKeywords($locale): string; + public function getKeywords(string $locale): ?string; /** * @return X509Certificate[] @@ -119,7 +119,7 @@ public function isRequestsMustBeSigned(): bool; /** * @return string */ - public function getManipulation(): string; + public function getManipulation(): ?string; /** * @return Coins diff --git a/src/OpenConext/EngineBlock/Metadata/Factory/ServiceProviderEntityInterface.php b/src/OpenConext/EngineBlock/Metadata/Factory/ServiceProviderEntityInterface.php index 925062a21..77b06a0a7 100644 --- a/src/OpenConext/EngineBlock/Metadata/Factory/ServiceProviderEntityInterface.php +++ b/src/OpenConext/EngineBlock/Metadata/Factory/ServiceProviderEntityInterface.php @@ -31,29 +31,16 @@ interface ServiceProviderEntityInterface { - /** - * @return null|int - */ public function getId(): ?int; - /** - * @return string - */ public function getEntityId(): string; - /** - * @param $locale - * @return string - */ - public function getName($locale): string; + public function getName(string $locale): ?string; - public function getDescription(string $locale): string; + public function getDescription(string $locale): ?string; - public function getDisplayName(string $locale): string; + public function getDisplayName(string $locale): ?string; - /** - * @return Logo|null - */ public function getLogo(): ?Logo; /** @@ -68,30 +55,19 @@ public function hasCompleteOrganizationData(string $locale): bool; */ public function getOrganization(string $locale): ?Organization; - /** - * @param $locale - * @return string - */ - public function getKeywords($locale): string; + public function getKeywords(string $locale): string; /** * @return X509Certificate[] */ public function getCertificates(): array; - /** - * @return string - */ public function getWorkflowState(): string; /** * @return ContactPerson[] */ public function getContactPersons(): array; - - /** - * @return null|string - */ public function getNameIdFormat(): ?string; /** @@ -99,20 +75,11 @@ public function getNameIdFormat(): ?string; */ public function getSupportedNameIdFormats(): array; - /** - * @return null|Service - */ public function getSingleLogoutService(): ?Service; - /** - * @return bool - */ public function isRequestsMustBeSigned(): bool; - /** - * @return string - */ - public function getManipulation(): string; + public function getManipulation(): ?string; /** * @return Coins @@ -134,9 +101,6 @@ public function getAssertionConsumerServices(): array; */ public function getAllowedIdpEntityIds(): array; - /** - * @return bool - */ public function isAllowAll(): bool; /** @@ -144,21 +108,10 @@ public function isAllowAll(): bool; */ public function getRequestedAttributes(): ?array; - /** - * @param $locale - * @return string|null - */ - public function getSupportUrl($locale): ?string; + public function getSupportUrl(string $locale): ?string; - /** - * @param string $idpEntityId - * @return bool - */ public function isAllowed(string $idpEntityId): bool; - /** - * @return bool - */ public function isAttributeAggregationRequired(): bool; public function getMdui(): Mdui; diff --git a/src/OpenConext/EngineBlock/Metadata/IndexedService.php b/src/OpenConext/EngineBlock/Metadata/IndexedService.php index b254c7cc9..a63cba09e 100644 --- a/src/OpenConext/EngineBlock/Metadata/IndexedService.php +++ b/src/OpenConext/EngineBlock/Metadata/IndexedService.php @@ -24,29 +24,31 @@ */ class IndexedService extends Service { - /** - * @var int - */ - public $serviceIndex; + public int $serviceIndex; /** * Note that null and false are NOT the same in this context. - * - * @var bool|null */ - public $isDefault = null; + public ?bool $isDefault = null; - /** - * @param string $location - * @param string $binding - * @param $serviceIndex - * @param bool|null $isDefault - */ - public function __construct($location, $binding, $serviceIndex, $isDefault = null) + public function __construct(string $location, string $binding, int $serviceIndex, ?bool $isDefault = null) { $this->isDefault = $isDefault; $this->serviceIndex = $serviceIndex; parent::__construct($location, $binding); } + + /** + * A convenience static constructor for the IndexedService. + */ + public static function indexedServiceFromArray(array $indexedService): IndexedService + { + return new self( + $indexedService["location"], + $indexedService["binding"], + $indexedService["serviceIndex"], + $indexedService["isDefault"] + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Logo.php b/src/OpenConext/EngineBlock/Metadata/Logo.php index 3bf21c142..431f50a54 100644 --- a/src/OpenConext/EngineBlock/Metadata/Logo.php +++ b/src/OpenConext/EngineBlock/Metadata/Logo.php @@ -27,16 +27,15 @@ */ class Logo implements MultilingualElement, JsonSerializable { - public $height = null; - public $width = null; - public $url = null; + public string $url; + public ?int $height; + public ?int $width; - /** - * @param string $url - */ - public function __construct($url) + public function __construct(string $url, ?int $height = null, ?int $width = null) { $this->url = $url; + $this->height = $height; + $this->width = $width; } public static function fromJson(array $multiLingualElement): MultilingualElement @@ -83,4 +82,12 @@ public function getConfiguredLanguages(): array { return [self::PRIMARY_LANGUAGE]; } + + /** + * A convenience static constructor for the Logo. + */ + public static function fromArray(array $logo): Logo + { + return new self($logo["url"], $logo["height"], $logo["width"]); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php index 5ee90ecc2..b98dab047 100644 --- a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php +++ b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php @@ -25,10 +25,17 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use OpenConext\EngineBlock\Metadata\Entity\AbstractRole; +use OpenConext\EngineBlock\Metadata\Entity\AbstractRoleEb5; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; +use OpenConext\EngineBlock\Metadata\Entity\IdentityProviderEb5; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlock\Metadata\Entity\ServiceProviderEb5; use RuntimeException; +/** + * // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + * @SuppressWarnings("CouplingBetweenObjects") + */ class DoctrineMetadataPushRepository { /** @@ -46,19 +53,32 @@ class DoctrineMetadataPushRepository */ private $idpMetadata; + /** + * @Deprecated + * @var ClassMetadata + */ + private $spMetadataDeprecated; - const ROLES_TABLE_NAME = 'sso_provider_roles_eb5'; + /** + * @Deprecated + * @var ClassMetadata + */ + private $idpMetadataDeprecated; const FIELD_VALUE = 0; const FIELD_TYPE = 1; public function __construct( - EntityManager $entityManager + EntityManager $entityManager, ) { $this->connection = $entityManager->getConnection(); $this->spMetadata = $entityManager->getClassMetadata(ServiceProvider::class); $this->idpMetadata = $entityManager->getClassMetadata(IdentityProvider::class); + + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $this->spMetadataDeprecated = $entityManager->getClassMetadata(ServiceProviderEb5::class); + $this->idpMetadataDeprecated = $entityManager->getClassMetadata(IdentityProviderEb5::class); } /** @@ -77,10 +97,12 @@ public function __construct( * @return SynchronizationResult * @throws \Exception */ - public function synchronize(array $roles) + public function synchronize(array $roles, array $rolesEb5): SynchronizationResult { - $result = new SynchronizationResult(); + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $result = $this->synchronizeOldTable($rolesEb5); + $result = new SynchronizationResult(); $this->connection->transactional(function () use ($roles, $result): void { $idpsToBeRemoved = $this->findAllRoleEntityIds($this->idpMetadata); $spsToBeRemoved = $this->findAllRoleEntityIds($this->spMetadata); @@ -146,23 +168,32 @@ public function synchronize(array $roles) return $result; } - private function insertRole(AbstractRole $role, ClassMetadata $metadata) + private function insertRole(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $metadata): void { + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $tableName = AbstractRole::TABLE_NAME; + if ($metadata === $this->idpMetadataDeprecated || $metadata === $this->spMetadataDeprecated) { + $tableName = AbstractRoleEb5::TABLE_NAME; + } $query = $this->connection->createQueryBuilder() - ->insert(self::ROLES_TABLE_NAME); + ->insert($tableName); $normalized = $this->addInsertQueryParameters($role, $query, $metadata); - $stmt = $this->connection->prepare($query->getSQL()); $this->bindParameters($normalized, $stmt); $stmt->executeQuery(); } - private function updateRole(AbstractRole $role, ClassMetadata $metadata) + private function updateRole(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $metadata): void { - $query = $this->connection->createQueryBuilder() - ->update(self::ROLES_TABLE_NAME); + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $tableName = AbstractRole::TABLE_NAME; + if ($metadata === $this->idpMetadataDeprecated || $metadata === $this->spMetadataDeprecated) { + $tableName = AbstractRoleEb5::TABLE_NAME; + } + $query = $this->connection->createQueryBuilder() + ->update($tableName); $normalized = $this->addUpdateQueryParameters($role, $query, $metadata); $stmt = $this->connection->prepare($query->getSQL()); @@ -170,10 +201,16 @@ private function updateRole(AbstractRole $role, ClassMetadata $metadata) $stmt->executeQuery(); } - private function deleteRolesByIds(array $roles, ClassMetadata $metadata) + private function deleteRolesByIds(array $roles, ClassMetadata $metadata): int|string { + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $tableName = AbstractRole::TABLE_NAME; + if ($metadata === $this->idpMetadataDeprecated || $metadata === $this->spMetadataDeprecated) { + $tableName = AbstractRoleEb5::TABLE_NAME; + } + $query = $this->connection->createQueryBuilder() - ->delete(self::ROLES_TABLE_NAME) + ->delete($tableName) ->where('id IN (:ids)') ->setParameter('ids', $roles, ArrayParameterType::INTEGER); @@ -183,11 +220,17 @@ private function deleteRolesByIds(array $roles, ClassMetadata $metadata) return $result; } - private function findAllRoleEntityIds(ClassMetadata $metadata) + private function findAllRoleEntityIds(ClassMetadata $metadata): array|null { + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $tableName = AbstractRole::TABLE_NAME; + if ($metadata === $this->idpMetadataDeprecated || $metadata === $this->spMetadataDeprecated) { + $tableName = AbstractRoleEb5::TABLE_NAME; + } + $query = $this->connection->createQueryBuilder() ->select('id, entity_id') - ->from(self::ROLES_TABLE_NAME); + ->from($tableName); assert($query instanceof QueryBuilder); $this->addDiscriminatorQuery($query, $metadata); @@ -202,7 +245,7 @@ private function findAllRoleEntityIds(ClassMetadata $metadata) return $results; } - private function addInsertQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata) + private function addInsertQueryParameters(AbstractRole|AbstractRoleEb5 $role, QueryBuilder $query, ClassMetadata $metadata): array { $normalized = $this->normalizeData($role, $metadata); foreach (array_keys($normalized) as $id) { @@ -211,7 +254,7 @@ private function addInsertQueryParameters(AbstractRole $role, QueryBuilder $quer return $normalized; } - private function addUpdateQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata) + private function addUpdateQueryParameters(AbstractRole|AbstractRoleEb5 $role, QueryBuilder $query, ClassMetadata $metadata): array { $normalized = $this->normalizeData($role, $metadata); foreach (array_keys($normalized) as $id) { @@ -224,20 +267,20 @@ private function addUpdateQueryParameters(AbstractRole $role, QueryBuilder $quer return $normalized; } - private function bindParameters($normalized, Statement $statement) + private function bindParameters($normalized, Statement $statement): void { foreach ($normalized as $id => $value) { $statement->bindValue($id, $value[self::FIELD_VALUE], $value[self::FIELD_TYPE]); } } - private function addDiscriminatorQuery(QueryBuilder $queryBuilder, ClassMetadata $metadata) + private function addDiscriminatorQuery(QueryBuilder $queryBuilder, ClassMetadata $metadata): void { $queryBuilder->andWhere(sprintf('%s = :%s', $metadata->discriminatorColumn->fieldName, $metadata->discriminatorColumn->name)) ->setParameter($metadata->discriminatorColumn->name, $metadata->discriminatorValue, $metadata->discriminatorColumn->type); } - private function normalizeData(AbstractRole $role, ClassMetadata $metadata) + private function normalizeData(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $metadata): array { $result = []; foreach ($metadata->fieldMappings as $id => $columnInfo) { @@ -257,4 +300,78 @@ private function normalizeData(AbstractRole $role, ClassMetadata $metadata) return $result; } + + /** + * TODO: Remove this code after sso_provider_roles_eb5 has been phased out + * + * @param array $rolesEb5 + * @return SynchronizationResult + * @throws \Throwable + */ + public function synchronizeOldTable(array $rolesEb5): SynchronizationResult + { + $result = new SynchronizationResult(); + $this->connection->transactional(function () use ($rolesEb5, $result): void { + $idpsToBeRemoved = $this->findAllRoleEntityIds($this->idpMetadataDeprecated); + $spsToBeRemoved = $this->findAllRoleEntityIds($this->spMetadataDeprecated); + + foreach ($rolesEb5 as $roleKey => $role) { + if ($role instanceof IdentityProviderEb5) { + // Does the IDP already exist in the database? + $index = array_search($role->entityId, $idpsToBeRemoved); + + if ($index === false) { + // The IDP is new: create it. + $this->insertRole($role, $this->idpMetadataDeprecated); + $result->createdIdentityProviders[] = $role->entityId; + } else { + // Remove from the list of entity ids so it won't get deleted later on. + unset($idpsToBeRemoved[$index]); + + // The IDP already exists: update it. + $role->id = $index; + $this->updateRole($role, $this->idpMetadataDeprecated); + $result->updatedIdentityProviders[] = $role->entityId; + } + unset($rolesEb5[$roleKey]); + continue; + } + + if ($role instanceof ServiceProviderEb5) { + // Does the SP already exist in the database? + $index = array_search($role->entityId, $spsToBeRemoved); + if ($index === false) { + // The SP is new: create it. + $this->insertRole($role, $this->spMetadataDeprecated); + $result->createdServiceProviders[] = $role->entityId; + } else { + // Remove from the list of entity ids so it won't get deleted later on. + unset($spsToBeRemoved[$index]); + + // The SP already exists: update it. + $role->id = $index; + $this->updateRole($role, $this->spMetadataDeprecated); + $result->updatedServiceProviders[] = $role->entityId; + } + unset($rolesEb5[$roleKey]); + continue; + } + + throw new RuntimeException( + sprintf('Unsupported role provided to synchronization: "%s"', var_export($role, true)) + ); + } + + if ($idpsToBeRemoved) { + $this->deleteRolesByIds(array_values($idpsToBeRemoved), $this->idpMetadataDeprecated); + $result->removedIdentityProviders = array_values($idpsToBeRemoved); + } + + if ($spsToBeRemoved) { + $this->deleteRolesByIds(array_values($spsToBeRemoved), $this->spMetadataDeprecated); + $result->removedServiceProviders = array_values($spsToBeRemoved); + } + }); + return $result; + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Organization.php b/src/OpenConext/EngineBlock/Metadata/Organization.php index a9073cdb0..96c772644 100644 --- a/src/OpenConext/EngineBlock/Metadata/Organization.php +++ b/src/OpenConext/EngineBlock/Metadata/Organization.php @@ -20,19 +20,26 @@ class Organization { - public $name; - public $displayName; - public $url; + public ?string $name; + public ?string $displayName; + public ?string $url; - /** - * @param $name - * @param $displayName - * @param $url - */ - public function __construct($name, $displayName, $url) + public function __construct(?string $name, ?string $displayName, ?string $url) { - $this->displayName = $displayName; $this->name = $name; + $this->displayName = $displayName; $this->url = $url; } + + /** + * A convenience static constructor for the Organization. + */ + public static function fromArray(array $organization): Organization + { + return new self( + $organization['name'] ?? null, + $organization['displayName'] ?? null, + $organization['url'] ?? null + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/RequestedAttribute.php b/src/OpenConext/EngineBlock/Metadata/RequestedAttribute.php index fb7abf5c7..d9c074ebe 100644 --- a/src/OpenConext/EngineBlock/Metadata/RequestedAttribute.php +++ b/src/OpenConext/EngineBlock/Metadata/RequestedAttribute.php @@ -26,30 +26,22 @@ class RequestedAttribute { const NAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'; - /** - * @var string - */ - public $name; - - /** - * @var string - */ - public $nameFormat = self::NAME_FORMAT_URI; - - /** - * @var null|bool - */ - public $required = null; + public string $name; + public ?bool $required = null; + public string $nameFormat = self::NAME_FORMAT_URI; - /** - * @param $name - * @param bool $isRequired - * @param string $nameFormat - */ - public function __construct($name, $isRequired = false, $nameFormat = self::NAME_FORMAT_URI) + public function __construct(string $name, ?bool $isRequired = false, string $nameFormat = self::NAME_FORMAT_URI) { $this->name = $name; $this->nameFormat = $nameFormat; $this->required = $isRequired; } + + /** + * A convenience static constructor for the RequestedAttribute. + */ + public static function fromArray(array $requestedAttribute): RequestedAttribute + { + return new self($requestedAttribute["name"], $requestedAttribute["required"], $requestedAttribute["nameFormat"]); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Service.php b/src/OpenConext/EngineBlock/Metadata/Service.php index 400bb5da1..a80d4faf9 100644 --- a/src/OpenConext/EngineBlock/Metadata/Service.php +++ b/src/OpenConext/EngineBlock/Metadata/Service.php @@ -24,23 +24,20 @@ */ class Service { - /** - * @var string - */ - public $binding; + public ?string $binding; + public ?string $location; - /** - * @var string - */ - public $location; + public function __construct(?string $location, ?string $binding) + { + $this->binding = $binding; + $this->location = $location; + } /** - * @param string $location - * @param string $binding + * A convenience static constructor for the Service. */ - public function __construct($location, $binding) + public static function fromArray(array $service): Service { - $this->binding = $binding; - $this->location = $location; + return new self($service["location"], $service["binding"]); } } diff --git a/src/OpenConext/EngineBlock/Metadata/ShibMdScope.php b/src/OpenConext/EngineBlock/Metadata/ShibMdScope.php index e1c5402ab..8eb426bca 100644 --- a/src/OpenConext/EngineBlock/Metadata/ShibMdScope.php +++ b/src/OpenConext/EngineBlock/Metadata/ShibMdScope.php @@ -24,13 +24,24 @@ */ class ShibMdScope { + public ?string $allowed; + public ?string $regexp; + /** - * @var string + * @param string|null $allowed + * @param string|null $regexp */ - public $allowed; + public function __construct(?string $allowed = null, ?string $regexp = null) + { + $this->allowed = $allowed; + $this->regexp = $regexp; + } /** - * @var string + * A convenience static constructor for the ShibMdScope. */ - public $regexp; + public static function fromArray(array $shibMdScope): ShibMdScope + { + return new self($shibMdScope["allowed"], $shibMdScope["regexp"]); + } } diff --git a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php index e90f795bf..07f08a5cf 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php @@ -122,6 +122,8 @@ public function pushConnectionsAction(Request $request) try { $roles = $this->pushMetadataAssembler->assemble($body->connections); + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out + $rolesEb5 = $this->pushMetadataAssembler->assembleEb5($body->connections); } catch (Exception $exception) { throw new BadApiRequestHttpException(sprintf('Unable to assemble the pushed metadata: %s', $exception->getMessage()), $exception); } @@ -129,7 +131,7 @@ public function pushConnectionsAction(Request $request) unset($body); try { - $result = $this->repository->synchronize($roles); + $result = $this->repository->synchronize($roles, $rolesEb5); } catch (Exception $exception) { throw new ApiInternalServerErrorHttpException('Unable to synchronize the assembled roles to the repository', $exception); } diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyType.php new file mode 100644 index 000000000..8b49b8e9c --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyType.php @@ -0,0 +1,103 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!$value instanceof AttributeReleasePolicy) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($value) ? get_class($value) : (string)$value, + $this->getName(), + "null, " . AttributeReleasePolicy::class + ) + ); + } + + return json_encode($value->getAttributeRules()); + } + + public function convertToPHPValue($value, AbstractPlatform $platform): ?AttributeReleasePolicy + { + if (is_null($value)) { + return $value; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $decoded, + $this->getName(), + "array" + ) + ); + } + + $arp = AttributeReleasePolicy::fromArray($decoded); + } catch (InvalidArgumentException | TypeError $e) { + // get nice standard message, so we can throw it keeping the exception chain + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + AttributeReleasePolicy::class + ), + 0, + $e + ); + } + + return $arp; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php new file mode 100644 index 000000000..f0cd590ef --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php @@ -0,0 +1,141 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + $value, + $this->getName(), + "null, array" + ) + ); + } + + if (count($value) == 0) { + return null; + } + + $certificates = []; + foreach ($value as $certificate) { + if (!$certificate instanceof X509CertificateLazyProxy) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + $certificate, + $this->getName(), + X509CertificateLazyProxy::class + ) + ); + } + array_push($certificates, $certificate->toCertData()); + } + + return json_encode($certificates); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $certificates = []; + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $decoded, + $this->getName(), + "array" + ) + ); + } + + foreach ($decoded as $certificate) { + if (is_string($certificate)) { + array_push($certificates, new X509CertificateLazyProxy(new X509CertificateFactory(), $certificate)); + } else { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $certificate, + $this->getName(), + "X509Certificate" + ) + ); + } + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + X509CertificateLazyProxy::class + ), + 0, + $e + ); + } + + return $certificates; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayType.php new file mode 100644 index 000000000..0afdf1921 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayType.php @@ -0,0 +1,136 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_array($value) ? 'Array' : (string)$value, + $this->getName(), + "null, array" + ) + ); + } + + if (empty($value)) { + return null; + } + + foreach ($value as $contactPerson) { + if (!$contactPerson instanceof ContactPerson) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($contactPerson) ? get_class($contactPerson) : (string)$contactPerson, + $this->getName(), + ContactPerson::class + ) + ); + } + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $contactPersons = []; + foreach ($decoded as $contactPerson) { + if (!is_array($contactPerson)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $contactPerson, + $this->getName(), + "array" + ) + ); + } + + array_push($contactPersons, ContactPerson::fromArray($contactPerson)); + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + ContactPerson::class + ), + 0, + $e + ); + } + + return $contactPersons; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayType.php new file mode 100644 index 000000000..a92dbba8f --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayType.php @@ -0,0 +1,136 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_array($value) ? 'Array' : (string)$value, + $this->getName(), + "null, array" + ) + ); + } + + if (empty($value)) { + return null; + } + + foreach ($value as $indexService) { + if (!$indexService instanceof IndexedService) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($indexService) ? get_class($indexService) : (string)$indexService, + $this->getName(), + IndexedService::class + ) + ); + } + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $indexedServices = []; + foreach ($decoded as $indexedService) { + if (!is_array($indexedService)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $indexedService, + $this->getName(), + "array" + ) + ); + } + + array_push($indexedServices, IndexedService::indexedServiceFromArray($indexedService)); + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + IndexedService::class + ), + 0, + $e + ); + } + + return $indexedServices; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/LogoType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/LogoType.php new file mode 100644 index 000000000..3cfb80359 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/LogoType.php @@ -0,0 +1,104 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!$value instanceof Logo) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($value) ? get_class($value) : (string)$value, + $this->getName(), + "null, " . Logo::class + ) + ); + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?Logo + { + if (is_null($value)) { + return null; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $logo = Logo::fromArray($decoded); + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + Logo::class + ), + 0, + $e + ); + } + + return $logo; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php new file mode 100644 index 000000000..97a1d3bac --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php @@ -0,0 +1,116 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!$value instanceof Organization) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($value) ? get_class($value) : (string)$value, + $this->getName(), + "null, " . Organization::class + ) + ); + } + + // Drop null properties regardless of what they're called + // This is done as many Organization(s) are potentially null or partially null + $data = array_filter( + get_object_vars($value), + static fn ($field) => $field !== null + ); + + // If every property was null, store NULL instead of "{}" + if ($data === []) { + return null; + } + + return json_encode($data); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?Organization + { + if (is_null($value)) { + return new Organization(null, null, null); + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $organization = Organization::fromArray($decoded); + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + Organization::class + ), + 0, + $e + ); + } + + return $organization; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayType.php new file mode 100644 index 000000000..58b0e9df6 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayType.php @@ -0,0 +1,136 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_array($value) ? 'Array' : (string)$value, + $this->getName(), + "null, array" + ) + ); + } + + if (empty($value)) { + return null; + } + + foreach ($value as $requestedAttribute) { + if (!$requestedAttribute instanceof RequestedAttribute) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($requestedAttribute) ? get_class($requestedAttribute) : (string)$requestedAttribute, + $this->getName(), + RequestedAttribute::class + ) + ); + } + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $requestedAttributes = []; + foreach ($decoded as $requestedAttribute) { + if (!is_array($requestedAttribute)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $requestedAttribute, + $this->getName(), + "array" + ) + ); + } + + array_push($requestedAttributes, RequestedAttribute::fromArray($requestedAttribute)); + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + RequestedAttribute::class + ), + 0, + $e + ); + } + + return $requestedAttributes; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayType.php new file mode 100644 index 000000000..4b15f396d --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayType.php @@ -0,0 +1,136 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_array($value) ? 'Array' : (string)$value, + $this->getName(), + "null, array" + ) + ); + } + + if (empty($value)) { + return null; + } + + foreach ($value as $service) { + if (!$service instanceof Service) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($service) ? get_class($service) : (string)$service, + $this->getName(), + Service::class + ) + ); + } + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $services = []; + foreach ($decoded as $service) { + if (!is_array($service)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $service, + $this->getName(), + "array" + ) + ); + } + + array_push($services, Service::fromArray($service)); + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + Service::class + ), + 0, + $e + ); + } + + return $services; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceType.php new file mode 100644 index 000000000..b3833b04d --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceType.php @@ -0,0 +1,103 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!$value instanceof Service) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($value) ? get_class($value) : (string)$value, + $this->getName(), + "null, " . Service::class + ) + ); + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?Service + { + if (is_null($value)) { + return null; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + $service = Service::fromArray($decoded); + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + Service::class + ), + 0, + $e + ); + } + + return $service; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayType.php new file mode 100644 index 000000000..691e867a2 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayType.php @@ -0,0 +1,136 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_array($value) ? 'Array' : (string)$value, + $this->getName(), + "null, array" + ) + ); + } + + if (empty($value)) { + return null; + } + + foreach ($value as $shibMdScope) { + if (!$shibMdScope instanceof ShibMdScope) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($shibMdScope) ? get_class($shibMdScope) : (string)$shibMdScope, + $this->getName(), + ShibMdScope::class + ) + ); + } + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $shibMdScopes = []; + foreach ($decoded as $shibMdScope) { + if (!is_array($shibMdScope)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $shibMdScope, + $this->getName(), + "array" + ) + ); + } + + array_push($shibMdScopes, ShibMdScope::fromArray($shibMdScope)); + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + ShibMdScope::class + ), + 0, + $e + ); + } + + return $shibMdScopes; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php index caccd0dbc..ccfba5d54 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php @@ -123,7 +123,7 @@ public function reset() { $queryBuilder = $this->entityManager->getConnection()->createQueryBuilder(); $queryBuilder - ->delete('sso_provider_roles_eb5') + ->delete(AbstractRole::TABLE_NAME) ->executeStatement(); return $this; @@ -133,7 +133,7 @@ public function remove($entityId, $role) { $queryBuilder = $this->entityManager->getConnection()->createQueryBuilder(); $queryBuilder - ->delete('sso_provider_roles_eb5') + ->delete(AbstractRole::TABLE_NAME) ->where('roles.entity_id = :entityId') ->andWhere('roles.type = :type') ->setParameter('entityId', $entityId) @@ -163,9 +163,10 @@ public function registerSp($name, $entityId, $acsLocation, $certData = '') // The repository does not allow us to retrieve all SP's for good reason. In functional testing mode the total // number of SP's should always be limited. + $tableName = AbstractRole::TABLE_NAME; $idpEntityIDQuery = <<entityManager->getConnection()->prepare($idpEntityIDQuery); @@ -200,9 +201,10 @@ public function registerIdp($name, $entityId, $ssoLocation, $certData = '', $dis // The repository does not allow us to retrieve all SP's for good reason. In functional testing mode the total // number of SP's should always be limited. + $tableName = AbstractRole::TABLE_NAME; $spEntityIDQuery = <<entityManager->getConnection()->prepare($spEntityIDQuery); diff --git a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/AttributeReleasePolicyControllerApiTest.php b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/AttributeReleasePolicyControllerApiTest.php index 1766d79ea..12d8535d1 100644 --- a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/AttributeReleasePolicyControllerApiTest.php +++ b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/AttributeReleasePolicyControllerApiTest.php @@ -20,6 +20,7 @@ use Doctrine\DBAL\Query\QueryBuilder; use OpenConext\EngineBlock\Metadata\AttributeReleasePolicy; +use OpenConext\EngineBlock\Metadata\Entity\AbstractRole; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -449,7 +450,7 @@ private function clearMetadataFixtures() $queryBuilder = self::getContainer()->get('doctrine')->getConnection()->createQueryBuilder(); assert($queryBuilder instanceof QueryBuilder); $queryBuilder - ->delete('sso_provider_roles_eb5') + ->delete(AbstractRole::TABLE_NAME) ->executeStatement() ; } diff --git a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsControllerTest.php b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsControllerTest.php index 73dcb4e8a..4d7503db7 100644 --- a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsControllerTest.php +++ b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsControllerTest.php @@ -20,6 +20,7 @@ use Doctrine\DBAL\Query\QueryBuilder; use Exception; +use OpenConext\EngineBlock\Metadata\Entity\AbstractRole; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use OpenConext\EngineBlock\Metadata\StepupConnections; @@ -471,7 +472,7 @@ private function clearMetadataFixtures() $queryBuilder = self::getContainer()->get('doctrine')->getConnection()->createQueryBuilder(); assert($queryBuilder instanceof QueryBuilder); $queryBuilder - ->delete('sso_provider_roles_eb5') + ->delete(AbstractRole::TABLE_NAME) ->executeStatement(); } diff --git a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConsentControllerTest.php b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConsentControllerTest.php index 525d4d5b8..30b81ece3 100644 --- a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConsentControllerTest.php +++ b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConsentControllerTest.php @@ -21,6 +21,7 @@ use DateTime; use Doctrine\DBAL\Query\QueryBuilder; use OpenConext\EngineBlock\Metadata\ContactPerson; +use OpenConext\EngineBlock\Metadata\Entity\AbstractRole; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use OpenConext\EngineBlock\Metadata\Mdui; use OpenConext\EngineBlock\Metadata\Organization; @@ -574,7 +575,7 @@ private function clearMetadataFixtures() $queryBuilder = self::getContainer()->get('doctrine')->getConnection()->createQueryBuilder(); assert($queryBuilder instanceof QueryBuilder); $queryBuilder - ->delete('sso_provider_roles_eb5') + ->delete(AbstractRole::TABLE_NAME) ->executeStatement(); } diff --git a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/MetadataControllerTest.php b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/MetadataControllerTest.php index 76ffe6330..20aaa5cec 100644 --- a/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/MetadataControllerTest.php +++ b/tests/functional/OpenConext/EngineBlockBundle/Controller/Api/MetadataControllerTest.php @@ -19,6 +19,7 @@ namespace OpenConext\EngineBlockBundle\Tests; use Doctrine\DBAL\Query\QueryBuilder; +use OpenConext\EngineBlock\Metadata\Entity\AbstractRole; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use OpenConext\EngineBlockBundle\Configuration\FeatureConfiguration; use PHPUnit\Framework\Attributes\Test; @@ -116,7 +117,7 @@ private function clearMetadataFixtures() $queryBuilder = self::getContainer()->get('doctrine')->getConnection()->createQueryBuilder(); assert($queryBuilder instanceof QueryBuilder); $queryBuilder - ->delete('sso_provider_roles_eb5') + ->delete(AbstractRole::TABLE_NAME) ->executeStatement(); } } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php new file mode 100644 index 000000000..c9548d46d --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php @@ -0,0 +1,137 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $value = $arpType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function attribute_release_policy_converted_to_json() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + $arp = array(); + $arp["uid"] = ["value" => "*", "motiviation" => ""]; + $arp["givenName"] = ["value" => "*", "motiviation" => ""]; + $arp["attribute"] = ["value" => "*", "motiviation" => ""]; + $attributeReleasePolicy = new AttributeReleasePolicy($arp); + + $value = $arpType->convertToDatabaseValue($attributeReleasePolicy, $this->platform); + + $this->assertEquals(json_encode($attributeReleasePolicy->getAttributeRules()), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_null() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $value = $arpType->convertToPHPValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $arp = array(); + $arp["uid"] = ["value" => "*", "motiviation" => ""]; + $arp["givenName"] = ["value" => "*", "motiviation" => ""]; + $arp["attribute"] = ["value" => "*", "motiviation" => ""]; + $attributeReleasePolicy = new AttributeReleasePolicy($arp); + + $value = $arpType->convertToPHPValue($arpType->convertToDatabaseValue($attributeReleasePolicy, $this->platform), + $this->platform); + + $this->assertEquals($attributeReleasePolicy, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $this->expectException(ConversionException::class); + $arpType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $this->expectException(ConversionException::class); + $arpType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php new file mode 100644 index 000000000..79e858553 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php @@ -0,0 +1,144 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_or_empty_array_is_null_conversion() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $value = $certType->convertToDatabaseValue(null, $this->platform); + $emptyArray = $certType->convertToDatabaseValue([], $this->platform); + + $this->assertNull($value); + $this->assertNull($emptyArray); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function certificate_array_type_converted_to_json() + { + $certType = Type::getType(CertificateArrayType::NAME); + $certificateArray = [new X509CertificateLazyProxy(new X509CertificateFactory(), $this->certData)]; + + $value = $certType->convertToDatabaseValue($certificateArray, $this->platform); + + $this->assertEquals(json_encode([$certificateArray[0]->toCertData()]), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $value = $certType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $certificateArray = [new X509CertificateLazyProxy(new X509CertificateFactory(), $this->certData)]; + + $value = $certType->convertToPHPValue($certType->convertToDatabaseValue($certificateArray, $this->platform), + $this->platform); + + $this->assertEquals($certificateArray[0]->toCertData(), $value[0]->toCertData()); + + // Verify that the certificate is actually valid. + $factory = new X509CertificateFactory(); + // baseline + $this->assertNotFalse(openssl_x509_read($factory->fromCertData($this->certData)->toPem())); + // converted + $this->assertNotFalse(openssl_x509_read($factory->fromCertData($value[0]->toCertData())->toPem())); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $this->expectException(ConversionException::class); + $this->expectExceptionMessage('Value "" must be null or an instance of engineblock_certificate_array (null, array) to be able to convert it to a database value'); + + $certType->convertToDatabaseValue("", $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $this->expectException(ConversionException::class); + $this->expectExceptionMessage('Could not convert database value "" to Doctrine Type engineblock_certificate_array. Expected format: X509Certificate'); + + $certType->convertToPHPValue("[false]", $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php new file mode 100644 index 000000000..270ad309d --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php @@ -0,0 +1,135 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $contactPersonType = Type::getType(ContactPersonArrayType::NAME); + + $value = $contactPersonType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function contact_person_array_type_converted_to_json() + { + $contactPersonType = Type::getType(ContactPersonArrayType::NAME); + $contactPerson = [new ContactPerson("support")]; + $contactPerson[0]->givenName = "givenName"; + $contactPerson[0]->telephoneNumber = "telephoneNumber"; + $contactPerson[0]->surName = "surName"; + $contactPerson[0]->emailAddress = "emailAddress"; + + $value = $contactPersonType->convertToDatabaseValue($contactPerson, $this->platform); + + $this->assertEquals(json_encode($contactPerson), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $contactPersonType = Type::getType(ContactPersonArrayType::NAME); + + $value = $contactPersonType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $contactPersonType = Type::getType(ContactPersonArrayType::NAME); + $contactPerson = [new ContactPerson("support")]; + $contactPerson[0]->givenName = "givenName"; + $contactPerson[0]->telephoneNumber = "telephoneNumber"; + $contactPerson[0]->surName = "surName"; + $contactPerson[0]->emailAddress = "emailAddress"; + + $value = $contactPersonType->convertToPHPValue($contactPersonType->convertToDatabaseValue($contactPerson, $this->platform), + $this->platform); + + $this->assertEquals($contactPerson, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $contactPersonType = Type::getType(ContactPersonArrayType::NAME); + + $this->expectException(ConversionException::class); + $contactPersonType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $contactPersonType = Type::getType(ContactPersonArrayType::NAME); + + $this->expectException(ConversionException::class); + $contactPersonType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php new file mode 100644 index 000000000..40dedb683 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php @@ -0,0 +1,127 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $indexedServiceArrayType = Type::getType(IndexedServiceArrayType::NAME); + + $value = $indexedServiceArrayType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function indexed_service_array_type_converted_to_json() + { + $indexedServiceArrayType = Type::getType(IndexedServiceArrayType::NAME); + $serviceIndex = [new IndexedService("location", "binding", 0)]; + $value = $indexedServiceArrayType->convertToDatabaseValue($serviceIndex, $this->platform); + + $this->assertEquals(json_encode($serviceIndex), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $indexedServiceArrayType = Type::getType(IndexedServiceArrayType::NAME); + + $value = $indexedServiceArrayType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $indexedServiceArrayType = Type::getType(IndexedServiceArrayType::NAME); + $serviceIndex = [new IndexedService("location", "binding", 0)]; + + $value = $indexedServiceArrayType->convertToPHPValue($indexedServiceArrayType->convertToDatabaseValue($serviceIndex, $this->platform), + $this->platform); + + $this->assertEquals($serviceIndex, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $indexedServiceArrayType = Type::getType(IndexedServiceArrayType::NAME); + + $this->expectException(ConversionException::class); + $indexedServiceArrayType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $indexedServiceArrayType = Type::getType(IndexedServiceArrayType::NAME); + + $this->expectException(ConversionException::class); + $indexedServiceArrayType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php new file mode 100644 index 000000000..be79b26a1 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php @@ -0,0 +1,127 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $logoType = Type::getType(LogoType::NAME); + + $value = $logoType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function logo_type_converted_to_json() + { + $logoType = Type::getType(LogoType::NAME); + $logo = new Logo("location"); + $value = $logoType->convertToDatabaseValue($logo, $this->platform); + + $this->assertEquals(json_encode($logo), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_null() + { + $logoType = Type::getType(LogoType::NAME); + + $value = $logoType->convertToPHPValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $logoType = Type::getType(LogoType::NAME); + $logo = new Logo("location"); + + $value = $logoType->convertToPHPValue($logoType->convertToDatabaseValue($logo, $this->platform), + $this->platform); + + $this->assertEquals($logo, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $logoType = Type::getType(LogoType::NAME); + + $this->expectException(ConversionException::class); + $logoType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $logoType = Type::getType(LogoType::NAME); + + $this->expectException(ConversionException::class); + $logoType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php new file mode 100644 index 000000000..5b0f14770 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php @@ -0,0 +1,176 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $value = $organizationType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_type_with_all_fields_converted_to_json() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", "displayName", "url"); + $value = $organizationType->convertToDatabaseValue($organization, $this->platform); + + $this->assertEquals(json_encode($organization), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_with_null_values_filters_them_out() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", null, "url"); + $value = $organizationType->convertToDatabaseValue($organization, $this->platform); + + // Should not contain the null displayName field + $decoded = json_decode($value, true); + $this->assertArrayNotHasKey('displayName', $decoded); + $this->assertArrayHasKey('name', $decoded); + $this->assertArrayHasKey('url', $decoded); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_with_all_null_values_returns_null() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization(null, null, null); + $value = $organizationType->convertToDatabaseValue($organization, $this->platform); + + // All null values should result in NULL being stored + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_organization_with_all_nulls() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $value = $organizationType->convertToPHPValue(null, $this->platform); + + $this->assertInstanceOf(Organization::class, $value); + $this->assertNull($value->name); + $this->assertNull($value->displayName); + $this->assertNull($value->url); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_with_partial_null_values_roundtrips_correctly() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", null, "url"); + + $databaseValue = $organizationType->convertToDatabaseValue($organization, $this->platform); + $value = $organizationType->convertToPHPValue($databaseValue, $this->platform); + + // After roundtrip, should have the non-null values + $this->assertEquals("name", $value->name); + $this->assertNull($value->displayName); + $this->assertEquals("url", $value->url); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_with_all_fields_equals_result() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", "displayName", "url"); + + $value = $organizationType->convertToPHPValue($organizationType->convertToDatabaseValue($organization, $this->platform), + $this->platform); + + $this->assertEquals($organization, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $this->expectException(ConversionException::class); + $organizationType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $this->expectException(ConversionException::class); + $organizationType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php new file mode 100644 index 000000000..571130ae1 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php @@ -0,0 +1,127 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $requestedAttributeArrayType = Type::getType(RequestedAttributeArrayType::NAME); + + $value = $requestedAttributeArrayType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function requested_attribute_array_type_converted_to_json() + { + $requestedAttributeArrayType = Type::getType(RequestedAttributeArrayType::NAME); + $requestedAttribute = [new RequestedAttribute("name")]; + $value = $requestedAttributeArrayType->convertToDatabaseValue($requestedAttribute, $this->platform); + + $this->assertEquals(json_encode($requestedAttribute), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $requestedAttributeArrayType = Type::getType(RequestedAttributeArrayType::NAME); + + $value = $requestedAttributeArrayType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $requestedAttributeArrayType = Type::getType(RequestedAttributeArrayType::NAME); + $requestedAttribute = [new RequestedAttribute("name")]; + + $value = $requestedAttributeArrayType->convertToPHPValue($requestedAttributeArrayType->convertToDatabaseValue($requestedAttribute, $this->platform), + $this->platform); + + $this->assertEquals($requestedAttribute, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $requestedAttributeArrayType = Type::getType(RequestedAttributeArrayType::NAME); + + $this->expectException(ConversionException::class); + $requestedAttributeArrayType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $requestedAttributeArrayType = Type::getType(RequestedAttributeArrayType::NAME); + + $this->expectException(ConversionException::class); + $requestedAttributeArrayType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php new file mode 100644 index 000000000..b66cb9308 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php @@ -0,0 +1,127 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $serviceArrayType = Type::getType(ServiceArrayType::NAME); + + $value = $serviceArrayType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function service_array_type_converted_to_json() + { + $serviceArrayType = Type::getType(ServiceArrayType::NAME); + $serviceArray = [new Service("location", "binding")]; + $value = $serviceArrayType->convertToDatabaseValue($serviceArray, $this->platform); + + $this->assertEquals(json_encode($serviceArray), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $serviceArrayType = Type::getType(ServiceArrayType::NAME); + + $value = $serviceArrayType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $serviceArrayType = Type::getType(ServiceArrayType::NAME); + $serviceArray = [new Service("location", "binding")]; + + $value = $serviceArrayType->convertToPHPValue($serviceArrayType->convertToDatabaseValue($serviceArray, $this->platform), + $this->platform); + + $this->assertEquals($serviceArray, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $serviceArrayType = Type::getType(ServiceArrayType::NAME); + + $this->expectException(ConversionException::class); + $serviceArrayType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $serviceArrayType = Type::getType(ServiceArrayType::NAME); + + $this->expectException(ConversionException::class); + $serviceArrayType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php new file mode 100644 index 000000000..25e9e121b --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php @@ -0,0 +1,142 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $serviceType = Type::getType(ServiceType::NAME); + + $value = $serviceType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function service_converted_to_json() + { + $serviceType = Type::getType(ServiceType::NAME); + $service = new Service("location", "binding"); + + $value = $serviceType->convertToDatabaseValue($service, $this->platform); + + $this->assertEquals(json_encode($service), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_null() + { + $serviceType = Type::getType(ServiceType::NAME); + + $value = $serviceType->convertToPHPValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $serviceType = Type::getType(ServiceType::NAME); + $serviceComplete = new Service("location", "binding"); + $serviceLocation = new Service("location", null); + $serviceBinding = new Service(null, "binding"); + + + $valueComplete = $serviceType->convertToPHPValue( + $serviceType->convertToDatabaseValue($serviceComplete, $this->platform), + $this->platform); + $valueLocation = $serviceType->convertToPHPValue( + $serviceType->convertToDatabaseValue($serviceLocation, $this->platform), + $this->platform); + $valueBinding = $serviceType->convertToPHPValue( + $serviceType->convertToDatabaseValue($serviceBinding, $this->platform), + $this->platform); + + + $this->assertEquals($serviceComplete, $valueComplete); + $this->assertEquals($serviceLocation, $valueLocation); + $this->assertEquals($serviceBinding, $valueBinding); + $this->assertNotEquals($valueBinding, $valueLocation); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $serviceType = Type::getType(ServiceType::NAME); + + $this->expectException(ConversionException::class); + $serviceType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $serviceType = Type::getType(ServiceType::NAME); + + $this->expectException(ConversionException::class); + $serviceType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php new file mode 100644 index 000000000..46009422e --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php @@ -0,0 +1,132 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $shibMdScopeArrayType = Type::getType(ShibMdScopeArrayType::NAME); + + $value = $shibMdScopeArrayType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function shib_md_scope_array_type_converted_to_json() + { + $shibMdScopeArrayType = Type::getType(ShibMdScopeArrayType::NAME); + $shibMdScopeArray = [new ShibMdScope()]; + $shibMdScopeArray[0]->regexp = true; + $shibMdScopeArray[0]->allowed = "query"; + + $value = $shibMdScopeArrayType->convertToDatabaseValue($shibMdScopeArray, $this->platform); + + $this->assertEquals(json_encode($shibMdScopeArray), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $shibMdScopeArrayType = Type::getType(ShibMdScopeArrayType::NAME); + + $value = $shibMdScopeArrayType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $shibMdScopeArrayType = Type::getType(ShibMdScopeArrayType::NAME); + $shibMdScopeArray = [new ShibMdScope()]; + $shibMdScopeArray[0]->regexp = true; + $shibMdScopeArray[0]->allowed = "query"; + + $value = $shibMdScopeArrayType->convertToPHPValue($shibMdScopeArrayType->convertToDatabaseValue($shibMdScopeArray, $this->platform), + $this->platform); + + $this->assertEquals($shibMdScopeArray, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $shibMdScopeArrayType = Type::getType(ShibMdScopeArrayType::NAME); + + $this->expectException(ConversionException::class); + $shibMdScopeArrayType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $shibMdScopeArrayType = Type::getType(ShibMdScopeArrayType::NAME); + + $this->expectException(ConversionException::class); + $shibMdScopeArrayType->convertToPHPValue(false, $this->platform); + } +}