From c232e223c90e47043b114fc19b9d6c2882345f89 Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Mon, 17 Aug 2026 15:31:03 +0200 Subject: [PATCH 1/7] Fix migrations - skipped migrations are not marked as run --- .../Version20260210000000.php | 15 ++++-------- .../Version20260224000000.php | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 20 deletions(-) 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`'); } From ce4516b6d02223337cc7a841904ce73d8a6f3dfd Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Mon, 17 Aug 2026 17:36:39 +0200 Subject: [PATCH 2/7] prepare for postgresql support --- config/packages/dev/monolog.yaml | 6 + config/packages/doctrine.yaml | 2 +- config/reference.php | 114 +++-- .../Version20260817133323.php | 80 ++++ .../Metadata/Entity/AbstractRole.php | 12 +- .../Metadata/Entity/AbstractRoleEb5.php | 347 +++++++++++++++ .../Assembler/PushMetadataAssembler.php | 195 ++++++++ .../Metadata/Entity/IdentityProviderEb5.php | 371 ++++++++++++++++ .../Metadata/Entity/ServiceProviderEb5.php | 416 ++++++++++++++++++ .../DoctrineMetadataPushRepository.php | 137 +++++- .../Controller/Api/ConnectionsController.php | 5 +- .../Doctrine/Type/CertificateArrayType.php | 142 ++++++ .../Fixtures/ServiceRegistryFixture.php | 10 +- ...ttributeReleasePolicyControllerApiTest.php | 3 +- .../Api/ConnectionsControllerTest.php | 3 +- .../Controller/Api/ConsentControllerTest.php | 3 +- .../Controller/Api/MetadataControllerTest.php | 3 +- .../Type/CertificateArrayTypeTest.php | 144 ++++++ 18 files changed, 1898 insertions(+), 95 deletions(-) create mode 100644 migrations/DoctrineMigrations/Version20260817133323.php create mode 100644 src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php create mode 100644 src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php create mode 100644 src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php 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..ae79f2b60 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -23,7 +23,7 @@ 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 array: OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType object: OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType legacy_json: OpenConext\EngineBlockBundle\Doctrine\Type\LegacyJsonType 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/Version20260817133323.php b/migrations/DoctrineMigrations/Version20260817133323.php new file mode 100644 index 000000000..f38e10043 --- /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 LONGTEXT NOT NULL, + mdui LONGTEXT 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 LONGTEXT 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/Entity/AbstractRole.php b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php index 62723b0d3..c5d9d1a4e 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php @@ -28,6 +28,7 @@ use OpenConext\EngineBlock\Metadata\Organization; use OpenConext\EngineBlock\Metadata\Service; use OpenConext\EngineBlock\Metadata\X509\X509Certificate; +use OpenConext\EngineBlockBundle\Doctrine\Type\CertificateArrayType; use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType; use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType; use RuntimeException; @@ -51,15 +52,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 @@ -184,7 +186,7 @@ abstract class AbstractRole /** * @var X509Certificate[] */ - #[ORM\Column(name: 'certificates', type: SerializedArrayType::NAME, length: 65535)] + #[ORM\Column(name: 'certificates', type: CertificateArrayType::NAME, length: 65535)] public $certificates = array(); /** 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/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/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/MetadataRepository/DoctrineMetadataPushRepository.php b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php index 5ee90ecc2..4f5a595f1 100644 --- a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php +++ b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php @@ -24,9 +24,13 @@ use Doctrine\DBAL\Statement; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; +use Monolog\Logger; 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; class DoctrineMetadataPushRepository @@ -46,19 +50,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 +94,73 @@ public function __construct( * @return SynchronizationResult * @throws \Exception */ - public function synchronize(array $roles) + public function synchronize(array $roles, array $rolesEb5): SynchronizationResult { + // TODO: Remove this code after sso_provider_roles_eb5 has been phased out $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); + } + }); + $result = new SynchronizationResult(); $this->connection->transactional(function () use ($roles, $result): void { $idpsToBeRemoved = $this->findAllRoleEntityIds($this->idpMetadata); $spsToBeRemoved = $this->findAllRoleEntityIds($this->spMetadata); @@ -146,23 +226,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 +259,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 +278,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 +303,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 +312,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 +325,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) { diff --git a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php index e90f795bf..e27facf8e 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php @@ -122,6 +122,9 @@ 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 +132,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/CertificateArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php new file mode 100644 index 000000000..f6f2b48b6 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php @@ -0,0 +1,142 @@ +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/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/CertificateArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php new file mode 100644 index 000000000..64ad203f6 --- /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); + } +} From b1c2c4a446826457b444d9bdda4175da34128ee0 Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Tue, 18 Aug 2026 09:50:25 +0200 Subject: [PATCH 3/7] deserialize database and set empty values null --- config/packages/doctrine.yaml | 10 ++ .../Metadata/AttributeReleasePolicy.php | 45 +++--- .../EngineBlock/Metadata/ContactPerson.php | 40 ++++- .../Metadata/Entity/AbstractRole.php | 101 ++++++------- .../Metadata/Entity/IdentityProvider.php | 39 ++--- .../Metadata/Entity/ServiceProvider.php | 40 ++--- .../Adapter/IdentityProviderEntity.php | 61 +------- .../Factory/Adapter/ServiceProviderEntity.php | 75 +-------- .../Decorator/AbstractServiceProvider.php | 73 +-------- .../IdentityProviderEntityInterface.php | 10 +- .../ServiceProviderEntityInterface.php | 59 +------- .../EngineBlock/Metadata/IndexedService.php | 29 ++-- src/OpenConext/EngineBlock/Metadata/Logo.php | 21 ++- .../EngineBlock/Metadata/Organization.php | 23 +-- .../Metadata/RequestedAttribute.php | 32 ++-- .../EngineBlock/Metadata/Service.php | 23 ++- .../EngineBlock/Metadata/ShibMdScope.php | 19 ++- .../Type/AttributeReleasePolicyType.php | 103 +++++++++++++ .../Doctrine/Type/ContactPersonArrayType.php | 136 +++++++++++++++++ .../Doctrine/Type/IndexedServiceArrayType.php | 136 +++++++++++++++++ .../Doctrine/Type/LogoType.php | 104 +++++++++++++ .../Doctrine/Type/OrganizationType.php | 104 +++++++++++++ .../Type/RequestedAttributeArrayType.php | 136 +++++++++++++++++ .../Doctrine/Type/ServiceArrayType.php | 136 +++++++++++++++++ .../Doctrine/Type/ServiceType.php | 103 +++++++++++++ .../Doctrine/Type/ShibMdScopeArrayType.php | 136 +++++++++++++++++ .../Type/AttributeReleasePolicyTypeTest.php | 137 +++++++++++++++++ .../Type/ContactPersonArrayTypeTest.php | 135 +++++++++++++++++ .../Type/IndexedServiceArrayTypeTest.php | 127 ++++++++++++++++ .../Doctrine/Type/LogoTypeTest.php | 127 ++++++++++++++++ .../Doctrine/Type/OrganizationTypeTest.php | 127 ++++++++++++++++ .../Type/RequestedAttributeArrayTypeTest.php | 127 ++++++++++++++++ .../Doctrine/Type/ServiceArrayTypeTest.php | 127 ++++++++++++++++ .../Doctrine/Type/ServiceTypeTest.php | 142 ++++++++++++++++++ .../Type/ShibMdScopeArrayTypeTest.php | 132 ++++++++++++++++ 35 files changed, 2537 insertions(+), 438 deletions(-) create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/LogoType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceType.php create mode 100644 src/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayType.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index ae79f2b60..ceb365aec 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -24,6 +24,16 @@ doctrine: 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 legacy_json: OpenConext\EngineBlockBundle\Doctrine\Type\LegacyJsonType 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..0d0431b7d 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,17 @@ 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 c5d9d1a4e..226206ee8 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php @@ -29,8 +29,10 @@ use OpenConext\EngineBlock\Metadata\Service; use OpenConext\EngineBlock\Metadata\X509\X509Certificate; use OpenConext\EngineBlockBundle\Doctrine\Type\CertificateArrayType; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType; +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; @@ -80,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: CertificateArrayType::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 @@ -210,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 @@ -228,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, @@ -252,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, @@ -272,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; @@ -308,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/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/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/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..72b88943a 100644 --- a/src/OpenConext/EngineBlock/Metadata/IndexedService.php +++ b/src/OpenConext/EngineBlock/Metadata/IndexedService.php @@ -24,29 +24,30 @@ */ 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/Organization.php b/src/OpenConext/EngineBlock/Metadata/Organization.php index a9073cdb0..e8710435d 100644 --- a/src/OpenConext/EngineBlock/Metadata/Organization.php +++ b/src/OpenConext/EngineBlock/Metadata/Organization.php @@ -20,19 +20,22 @@ 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"], $organization["displayName"], $organization["url"]); + } } 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/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/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..c08d7d666 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.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 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 + ) + ); + } + + return json_encode($value); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?Organization + { + 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" + ) + ); + } + + $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/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php new file mode 100644 index 000000000..900878d9d --- /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/ContactPersonArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php new file mode 100644 index 000000000..ee06baef5 --- /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..44cd44a8f --- /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..6ae127e36 --- /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..7b9cf392e --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php @@ -0,0 +1,127 @@ +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_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 a_null_value_is_converted_to_null() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $value = $organizationType->convertToPHPValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_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..915af93f7 --- /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..036aa2d7d --- /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..0777d8d55 --- /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..a1ae33d36 --- /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); + } +} From f6cd2267a6d078a60a58da75ac0a8d5dd88e9508 Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Tue, 18 Aug 2026 15:58:28 +0200 Subject: [PATCH 4/7] Correct database types for columns and make OrganizationType clearer --- .../Version20260817133323.php | 6 +- .../EngineBlock/Metadata/Organization.php | 6 +- .../Doctrine/Type/OrganizationType.php | 16 +++++- .../Doctrine/Type/OrganizationTypeTest.php | 57 +++++++++++++++++-- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/migrations/DoctrineMigrations/Version20260817133323.php b/migrations/DoctrineMigrations/Version20260817133323.php index f38e10043..41af23698 100644 --- a/migrations/DoctrineMigrations/Version20260817133323.php +++ b/migrations/DoctrineMigrations/Version20260817133323.php @@ -48,8 +48,8 @@ public function up(Schema $schema): void single_logout_service JSON DEFAULT NULL, requests_must_be_signed TINYINT NOT NULL, manipulation TEXT, - coins LONGTEXT NOT NULL, - mdui LONGTEXT NOT NULL, + 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, @@ -63,7 +63,7 @@ public function up(Schema $schema): void single_sign_on_services JSON DEFAULT NULL, consent_settings LONGTEXT DEFAULT NULL, shib_md_scopes JSON DEFAULT NULL, - idp_discoveries LONGTEXT 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), diff --git a/src/OpenConext/EngineBlock/Metadata/Organization.php b/src/OpenConext/EngineBlock/Metadata/Organization.php index e8710435d..96c772644 100644 --- a/src/OpenConext/EngineBlock/Metadata/Organization.php +++ b/src/OpenConext/EngineBlock/Metadata/Organization.php @@ -36,6 +36,10 @@ public function __construct(?string $name, ?string $displayName, ?string $url) */ public static function fromArray(array $organization): Organization { - return new self($organization["name"], $organization["displayName"], $organization["url"]); + return new self( + $organization['name'] ?? null, + $organization['displayName'] ?? null, + $organization['url'] ?? null + ); } } diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php index c08d7d666..97a1d3bac 100644 --- a/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php @@ -54,7 +54,19 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str ); } - return json_encode($value); + // 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); } /** @@ -63,7 +75,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str public function convertToPHPValue($value, AbstractPlatform $platform): ?Organization { if (is_null($value)) { - return null; + return new Organization(null, null, null); } try { diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php index 7b9cf392e..8a4e940cf 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php @@ -68,7 +68,7 @@ public function a_null_value_remains_null_in_to_sql_conversion() #[Group('EngineBlockBundle')] #[Group('Doctrine')] #[Test] - public function organization_type_converted_to_json() + public function organization_type_with_all_fields_converted_to_json() { $organizationType = Type::getType(OrganizationType::NAME); $organization = new Organization("name", "displayName", "url"); @@ -80,19 +80,68 @@ public function organization_type_converted_to_json() #[Group('EngineBlockBundle')] #[Group('Doctrine')] #[Test] - public function a_null_value_is_converted_to_null() + 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); - $value = $organizationType->convertToPHPValue(null, $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 saved_object_equals_result() + 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"); From 3e60751a741297c018ba1c3b3e7ae2bd1db1f13b Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Mon, 24 Aug 2026 10:24:31 +0200 Subject: [PATCH 5/7] Refactor for quality checks and use of correct casing --- .../EngineBlock/Metadata/ContactPerson.php | 6 +- .../EngineBlock/Metadata/IndexedService.php | 3 +- .../DoctrineMetadataPushRepository.php | 142 ++++++++++-------- .../Controller/Api/ConnectionsController.php | 1 - .../Doctrine/Type/CertificateArrayType.php | 1 - .../Type/AttributeReleasePolicyTypeTest.php | 6 +- .../Type/CertificateArrayTypeTest.php | 6 +- .../Type/ContactPersonArrayTypeTest.php | 6 +- .../Type/IndexedServiceArrayTypeTest.php | 6 +- .../Doctrine/Type/LogoTypeTest.php | 6 +- .../Doctrine/Type/OrganizationTypeTest.php | 6 +- .../Type/RequestedAttributeArrayTypeTest.php | 6 +- .../Doctrine/Type/ServiceArrayTypeTest.php | 6 +- .../Doctrine/Type/ServiceTypeTest.php | 6 +- .../Type/ShibMdScopeArrayTypeTest.php | 6 +- 15 files changed, 115 insertions(+), 98 deletions(-) diff --git a/src/OpenConext/EngineBlock/Metadata/ContactPerson.php b/src/OpenConext/EngineBlock/Metadata/ContactPerson.php index 0d0431b7d..ea7bcf593 100644 --- a/src/OpenConext/EngineBlock/Metadata/ContactPerson.php +++ b/src/OpenConext/EngineBlock/Metadata/ContactPerson.php @@ -81,10 +81,12 @@ public static function from( */ public static function fromArray(array $contactPerson): ContactPerson { - return new self($contactPerson["contactType"], + return new self( + $contactPerson["contactType"], $contactPerson["emailAddress"], $contactPerson["telephoneNumber"], $contactPerson["givenName"], - $contactPerson["surName"]); + $contactPerson["surName"] + ); } } diff --git a/src/OpenConext/EngineBlock/Metadata/IndexedService.php b/src/OpenConext/EngineBlock/Metadata/IndexedService.php index 72b88943a..a63cba09e 100644 --- a/src/OpenConext/EngineBlock/Metadata/IndexedService.php +++ b/src/OpenConext/EngineBlock/Metadata/IndexedService.php @@ -44,7 +44,8 @@ public function __construct(string $location, string $binding, int $serviceIndex */ public static function indexedServiceFromArray(array $indexedService): IndexedService { - return new self($indexedService["location"], + return new self( + $indexedService["location"], $indexedService["binding"], $indexedService["serviceIndex"], $indexedService["isDefault"] diff --git a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php index 4f5a595f1..b98dab047 100644 --- a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php +++ b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php @@ -24,7 +24,6 @@ use Doctrine\DBAL\Statement; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; -use Monolog\Logger; use OpenConext\EngineBlock\Metadata\Entity\AbstractRole; use OpenConext\EngineBlock\Metadata\Entity\AbstractRoleEb5; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; @@ -33,6 +32,10 @@ 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 { /** @@ -97,68 +100,7 @@ public function __construct( public function synchronize(array $roles, array $rolesEb5): SynchronizationResult { // TODO: Remove this code after sso_provider_roles_eb5 has been phased out - $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); - } - }); + $result = $this->synchronizeOldTable($rolesEb5); $result = new SynchronizationResult(); $this->connection->transactional(function () use ($roles, $result): void { @@ -358,4 +300,78 @@ private function normalizeData(AbstractRole|AbstractRoleEb5 $role, ClassMetadata 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/EngineBlockBundle/Controller/Api/ConnectionsController.php b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php index e27facf8e..07f08a5cf 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php @@ -124,7 +124,6 @@ public function pushConnectionsAction(Request $request) $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); } diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php index f6f2b48b6..f0cd590ef 100644 --- a/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php @@ -26,7 +26,6 @@ use OpenConext\EngineBlock\Exception\InvalidArgumentException; use OpenConext\EngineBlock\Metadata\X509\X509CertificateFactory; use OpenConext\EngineBlock\Metadata\X509\X509CertificateLazyProxy; -use TypeError; class CertificateArrayType extends Type { diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php index 900878d9d..c9548d46d 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class AttributeReleasePolicyTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php index 64ad203f6..79e858553 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php @@ -18,7 +18,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class CertificateArrayTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -52,7 +52,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php index ee06baef5..270ad309d 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php @@ -18,7 +18,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -32,7 +32,7 @@ class ContactPersonArrayTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -49,7 +49,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php index 44cd44a8f..40dedb683 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/IndexedServiceArrayTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class IndexedServiceArrayTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php index 6ae127e36..be79b26a1 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/LogoTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class LogoTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php index 8a4e940cf..5b0f14770 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class OrganizationTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php index 915af93f7..571130ae1 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class RequestedAttributeArrayTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php index 036aa2d7d..b66cb9308 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceArrayTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class ServiceArrayTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php index 0777d8d55..25e9e121b 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ServiceTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class ServiceTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php index a1ae33d36..46009422e 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ShibMdScopeArrayTypeTest.php @@ -19,7 +19,7 @@ namespace OpenConext\EngineBlockBundle\Doctrine\Type; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -33,7 +33,7 @@ class ShibMdScopeArrayTypeTest extends TestCase use MockeryPHPUnitIntegration; /** - * @var MySqlPlatform + * @var MySQLPlatform */ private $platform; @@ -50,7 +50,7 @@ public static function setUpBeforeClass(): void public function setUp(): void { - $this->platform = new MySqlPlatform(); + $this->platform = new MySQLPlatform(); } #[Group('EngineBlockBundle')] From ab0cc4949c4e2075e835c93c3594071617ce355c Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Mon, 24 Aug 2026 12:11:50 +0200 Subject: [PATCH 6/7] Remove Eb5 and add postgres support --- composer.json | 2 +- config/packages/ci/doctrine.yaml | 12 +- config/packages/ci/parameters.yml | 20 + config/packages/dev/parameters.yml | 11 + config/packages/doctrine.yaml | 4 +- config/packages/doctrine_migrations.yaml | 3 +- config/packages/parameters.yml.dist | 2 + .../postgres/doctrine_migrations.yaml | 4 + config/packages/postgres/parameters.yml | 15 + config/packages/test/doctrine.yaml | 8 +- docker/docker-compose.yml | 2 +- .../AbstractEngineBlockMigration.php | 2 +- .../Version20260210000000.php | 0 .../Version20260224000000.php | 0 .../Version20260315000001.php | 0 .../Version20260331000000.php | 0 .../Version20260602000000.php | 0 .../Version20260817133323.php | 0 .../MariaMigrations/Version20260818111645.php | 73 +++ .../Version20260814091601.php | 128 ++++++ postgres_support.md | 180 ++++++++ .../Metadata/Entity/AbstractRoleEb5.php | 347 --------------- .../Assembler/PushMetadataAssembler.php | 195 -------- .../Metadata/Entity/IdentityProviderEb5.php | 371 ---------------- .../Metadata/Entity/ServiceProviderEb5.php | 416 ------------------ .../DoctrineMetadataPushRepository.php | 162 +------ .../Authentication/Entity/User.php | 18 +- .../Repository/DbalConsentRepository.php | 83 +++- .../Controller/Api/ConnectionsController.php | 4 +- .../Fixtures/ServiceRegistryFixture.php | 16 +- 30 files changed, 563 insertions(+), 1515 deletions(-) create mode 100644 config/packages/postgres/doctrine_migrations.yaml create mode 100644 config/packages/postgres/parameters.yml rename migrations/{DoctrineMigrations => MariaMigrations}/AbstractEngineBlockMigration.php (94%) rename migrations/{DoctrineMigrations => MariaMigrations}/Version20260210000000.php (100%) rename migrations/{DoctrineMigrations => MariaMigrations}/Version20260224000000.php (100%) rename migrations/{DoctrineMigrations => MariaMigrations}/Version20260315000001.php (100%) rename migrations/{DoctrineMigrations => MariaMigrations}/Version20260331000000.php (100%) rename migrations/{DoctrineMigrations => MariaMigrations}/Version20260602000000.php (100%) rename migrations/{DoctrineMigrations => MariaMigrations}/Version20260817133323.php (100%) create mode 100644 migrations/MariaMigrations/Version20260818111645.php create mode 100644 migrations/PostgresMigrations/Version20260814091601.php create mode 100644 postgres_support.md delete mode 100644 src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php delete mode 100644 src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php delete mode 100644 src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php diff --git a/composer.json b/composer.json index 1dbb587ba..c88801f29 100644 --- a/composer.json +++ b/composer.json @@ -112,7 +112,7 @@ }, "psr-4": { "OpenConext\\": "src/OpenConext", - "OpenConext\\EngineBlock\\Doctrine\\Migrations\\": "migrations/DoctrineMigrations" + "OpenConext\\EngineBlock\\Doctrine\\Migrations\\": "migrations/MariaMigrations" }, "classmap": [ "src/Kernel.php" diff --git a/config/packages/ci/doctrine.yaml b/config/packages/ci/doctrine.yaml index 26f9ce1f9..97692e383 100644 --- a/config/packages/ci/doctrine.yaml +++ b/config/packages/ci/doctrine.yaml @@ -3,16 +3,16 @@ doctrine: default_connection: engineblock_test connections: engineblock: - driver: pdo_mysql # This must be PDO until all database interaction runs through doctrine - server_version: '10.6.0-MariaDB' + driver: "%database.test.driver%" + server_version: "%database.test.server_version%" dbname: "%database.dbname%" host: "%database.test.host%" - port: "%database.port%" - user: "%database.user%" + port: "%database.port%" + user: "%database.user%" password: "%database.password%" engineblock_test: - driver: pdo_mysql # This must be PDO until all database interaction runs through doctrine - server_version: '10.6.0-MariaDB' + driver: "%database.test.driver%" + server_version: "%database.test.server_version%" dbname: "%database.test.dbname%" host: "%database.test.host%" port: "%database.test.port%" diff --git a/config/packages/ci/parameters.yml b/config/packages/ci/parameters.yml index b6faf85f6..a0d49ceb8 100644 --- a/config/packages/ci/parameters.yml +++ b/config/packages/ci/parameters.yml @@ -12,3 +12,23 @@ parameters: rollover: publicFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.crt' privateFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.key' + + database.test.driver: pdo_mysql + database.test.server_version: 10.11.13-MariaDB + +# For postgres database +#parameters: +# database.driver: pdo_pgsql +# database.server_version: '17' +# database.host: postgres +# database.port: '5432' +# database.user: ebrw +# database.password: secret +# database.dbname: eb +# database.test.driver: pdo_pgsql +# database.test.server_version: '17' +# database.test.host: postgres +# database.test.port: '5432' +# database.test.user: eb_testrw +# database.test.password: secret +# database.test.dbname: eb_test diff --git a/config/packages/dev/parameters.yml b/config/packages/dev/parameters.yml index 86a61fcf8..020dc619e 100644 --- a/config/packages/dev/parameters.yml +++ b/config/packages/dev/parameters.yml @@ -9,3 +9,14 @@ parameters: rollover: publicFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.crt' privateFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.key' + +# For postgres database +#parameters: +# database.driver: pdo_pgsql +# database.server_version: '17' +# database.host: postgres +# database.port: '5432' +# database.user: ebrw +# database.password: secret +# database.dbname: eb + diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index ceb365aec..7882bda65 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -5,7 +5,7 @@ doctrine: connections: engineblock: # schema_filter: ~^(?!group_|virtual_|service_provider_|saml_persistent_id|sso_provider_roles|log_logins|db_changelog|consent)~ - driver: pdo_mysql # This must be PDO until all database interaction runs through doctrine + driver: "%database.driver%" dbname: "%database.dbname%" host: "%database.host%" port: "%database.port%" @@ -15,7 +15,7 @@ doctrine: # when true, queries are logged to a 'doctrine' monolog channel logging: '%kernel.debug%' profiling: '%kernel.debug%' - server_version: '10.11.13-MariaDB' + server_version: "%database.server_version%" mapping_types: enum: string types: diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml index 0c232efcb..8a87edf49 100644 --- a/config/packages/doctrine_migrations.yaml +++ b/config/packages/doctrine_migrations.yaml @@ -1,6 +1,7 @@ doctrine_migrations: migrations_paths: - OpenConext\EngineBlock\Doctrine\Migrations: '%kernel.project_dir%/migrations/DoctrineMigrations' + OpenConext\EngineBlock\Doctrine\Migrations: + '%kernel.project_dir%/migrations/MariaMigrations' storage: table_storage: table_name: 'migration_versions' diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index 18212e78e..c3774a0fa 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -126,6 +126,8 @@ parameters: ########################################################################################## ## DATABASE SETTINGS ########################################################################################## + database.driver: pdo_mysql + database.server_version: '10.11.13-MariaDB' database.host: mariadb database.port: '3306' database.user: ebrw diff --git a/config/packages/postgres/doctrine_migrations.yaml b/config/packages/postgres/doctrine_migrations.yaml new file mode 100644 index 000000000..3583e0dbc --- /dev/null +++ b/config/packages/postgres/doctrine_migrations.yaml @@ -0,0 +1,4 @@ +doctrine_migrations: + migrations_paths: + OpenConext\EngineBlock\Doctrine\Migrations: + '%kernel.project_dir%/migrations/PostgresMigrations' diff --git a/config/packages/postgres/parameters.yml b/config/packages/postgres/parameters.yml new file mode 100644 index 000000000..8c3bdc272 --- /dev/null +++ b/config/packages/postgres/parameters.yml @@ -0,0 +1,15 @@ +parameters: + database.driver: pdo_pgsql + database.server_version: '17' + database.host: postgres + database.port: '5432' + database.user: ebrw + database.password: secret + database.dbname: eb + database.test.driver: pdo_pgsql + database.test.server_version: '17' + database.test.host: postgres + database.test.port: '5432' + database.test.user: eb_testrw + database.test.password: secret + database.test.dbname: eb_test diff --git a/config/packages/test/doctrine.yaml b/config/packages/test/doctrine.yaml index 448b48047..9759d3392 100644 --- a/config/packages/test/doctrine.yaml +++ b/config/packages/test/doctrine.yaml @@ -3,13 +3,15 @@ doctrine: default_connection: engineblock_test connections: engineblock_test: - driver: pdo_mysql # This must be PDO until all database interaction runs through doctrine - server_version: '10.6.0-MariaDB' + driver: "%database.test.driver%" + server_version: "%database.test.server_version%" dbname: "%database.test.dbname%" host: "%database.test.host%" port: "%database.test.port%" user: "%database.test.user%" password: "%database.test.password%" # -#parameters: +parameters: + database.test.driver: pdo_mysql + database.test.server_version: 10.11.13-MariaDB # database.test.host: 127.0.0.1 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 13bb3bc86..dc5d53f4f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,7 +1,7 @@ services: mariadb: - image: mariadb:10.6 + image: mariadb:10.11 container_name: eb-db-test environment: MYSQL_ROOT_PASSWORD: "root" diff --git a/migrations/DoctrineMigrations/AbstractEngineBlockMigration.php b/migrations/MariaMigrations/AbstractEngineBlockMigration.php similarity index 94% rename from migrations/DoctrineMigrations/AbstractEngineBlockMigration.php rename to migrations/MariaMigrations/AbstractEngineBlockMigration.php index bd047836b..587ce772a 100644 --- a/migrations/DoctrineMigrations/AbstractEngineBlockMigration.php +++ b/migrations/MariaMigrations/AbstractEngineBlockMigration.php @@ -28,7 +28,7 @@ /** * Base class for all EngineBlock Doctrine migrations. * - * All migrations in this project target MariaDB exclusively. The generated DDL SQL is platform-specific + * All migrations in this project target MariaDbMigrations exclusively. The generated DDL SQL is platform-specific * and is not guaranteed to be compatible with MySQL or any other database engine. * */ diff --git a/migrations/DoctrineMigrations/Version20260210000000.php b/migrations/MariaMigrations/Version20260210000000.php similarity index 100% rename from migrations/DoctrineMigrations/Version20260210000000.php rename to migrations/MariaMigrations/Version20260210000000.php diff --git a/migrations/DoctrineMigrations/Version20260224000000.php b/migrations/MariaMigrations/Version20260224000000.php similarity index 100% rename from migrations/DoctrineMigrations/Version20260224000000.php rename to migrations/MariaMigrations/Version20260224000000.php diff --git a/migrations/DoctrineMigrations/Version20260315000001.php b/migrations/MariaMigrations/Version20260315000001.php similarity index 100% rename from migrations/DoctrineMigrations/Version20260315000001.php rename to migrations/MariaMigrations/Version20260315000001.php diff --git a/migrations/DoctrineMigrations/Version20260331000000.php b/migrations/MariaMigrations/Version20260331000000.php similarity index 100% rename from migrations/DoctrineMigrations/Version20260331000000.php rename to migrations/MariaMigrations/Version20260331000000.php diff --git a/migrations/DoctrineMigrations/Version20260602000000.php b/migrations/MariaMigrations/Version20260602000000.php similarity index 100% rename from migrations/DoctrineMigrations/Version20260602000000.php rename to migrations/MariaMigrations/Version20260602000000.php diff --git a/migrations/DoctrineMigrations/Version20260817133323.php b/migrations/MariaMigrations/Version20260817133323.php similarity index 100% rename from migrations/DoctrineMigrations/Version20260817133323.php rename to migrations/MariaMigrations/Version20260817133323.php diff --git a/migrations/MariaMigrations/Version20260818111645.php b/migrations/MariaMigrations/Version20260818111645.php new file mode 100644 index 000000000..4a04554ea --- /dev/null +++ b/migrations/MariaMigrations/Version20260818111645.php @@ -0,0 +1,73 @@ +addSql('DROP TABLE sso_provider_roles_eb5'); + } + + public function down(Schema $schema): void + { + $this->addSql('CREATE TABLE `sso_provider_roles_eb5` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `entity_id` varchar(255) NOT NULL, + `name_nl` varchar(255) NOT NULL, + `name_en` varchar(255) NOT NULL, + `name_pt` varchar(255) NOT NULL, + `description_nl` varchar(255) NOT NULL, + `description_en` varchar(255) NOT NULL, + `description_pt` varchar(255) NOT NULL, + `display_name_nl` varchar(255) NOT NULL, + `display_name_en` varchar(255) NOT NULL, + `display_name_pt` varchar(255) NOT NULL, + `logo` longtext NOT NULL COMMENT \'(DC2Type:object)\', + `organization_nl_name` text DEFAULT NULL COMMENT \'(DC2Type:object)\', + `organization_en_name` text DEFAULT NULL COMMENT \'(DC2Type:object)\', + `organization_pt_name` text DEFAULT NULL COMMENT \'(DC2Type:object)\', + `keywords_nl` varchar(255) NOT NULL, + `keywords_en` varchar(255) NOT NULL, + `keywords_pt` varchar(255) NOT NULL, + `certificates` text NOT NULL COMMENT \'(DC2Type:array)\', + `workflow_state` varchar(255) NOT NULL, + `contact_persons` text NOT NULL COMMENT \'(DC2Type:array)\', + `name_id_format` varchar(255) DEFAULT NULL, + `name_id_formats` text NOT NULL COMMENT \'(DC2Type:array)\', + `single_logout_service` text DEFAULT NULL COMMENT \'(DC2Type:object)\', + `requests_must_be_signed` tinyint(1) NOT NULL, + `manipulation` text NOT NULL, + `type` varchar(255) NOT NULL, + `attribute_release_policy` text DEFAULT NULL COMMENT \'(DC2Type:array)\', + `assertion_consumer_services` text DEFAULT NULL COMMENT \'(DC2Type:array)\', + `allowed_idp_entity_ids` mediumtext DEFAULT NULL COMMENT \'(DC2Type:array)\', + `allow_all` tinyint(1) DEFAULT NULL, + `requested_attributes` text DEFAULT NULL COMMENT \'(DC2Type:array)\', + `enabled_in_wayf` tinyint(1) DEFAULT NULL, + `single_sign_on_services` text DEFAULT NULL COMMENT \'(DC2Type:array)\', + `shib_md_scopes` text DEFAULT NULL COMMENT \'(DC2Type:array)\', + `support_url_en` varchar(255) DEFAULT NULL, + `support_url_pt` varchar(255) DEFAULT NULL, + `support_url_nl` varchar(255) DEFAULT NULL, + `consent_settings` longtext DEFAULT NULL COMMENT \'(DC2Type:json)\', + `coins` longtext NOT NULL COMMENT \'(DC2Type:engineblock_metadata_coins)\', + `mdui` longtext NOT NULL COMMENT \'(DC2Type:engineblock_metadata_mdui)\', + `idp_discoveries` longtext DEFAULT NULL COMMENT \'(DC2Type:json)\', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_sso_provider_roles_entity_id_type` (`type`,`entity_id`), + KEY `idx_sso_provider_roles_type` (`type`), + KEY `idx_sso_provider_roles_entity_id` (`entity_id`) + ) ENGINE=InnoDB AUTO_INCREMENT=63268 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci'); + } +} diff --git a/migrations/PostgresMigrations/Version20260814091601.php b/migrations/PostgresMigrations/Version20260814091601.php new file mode 100644 index 000000000..f10642b9b --- /dev/null +++ b/migrations/PostgresMigrations/Version20260814091601.php @@ -0,0 +1,128 @@ +addSql(<<<'SQL' + CREATE TABLE consent ( + consent_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, + hashed_user_id VARCHAR(80) NOT NULL, + service_id VARCHAR(255) NOT NULL, + attribute VARCHAR(80) NOT NULL, + attribute_stable VARCHAR(80) DEFAULT NULL, + consent_type VARCHAR(20) DEFAULT 'explicit' NOT NULL, + deleted_at TIMESTAMP(0) WITHOUT TIME ZONE, + PRIMARY KEY (hashed_user_id, service_id, deleted_at) + ) + SQL); + $this->addSql('CREATE INDEX consent_hashed_user_id_idx ON consent (hashed_user_id)'); + $this->addSql('CREATE INDEX consent_service_id_idx ON consent (service_id)'); + + $this->addSql(<<<'SQL' + CREATE TABLE saml_persistent_id ( + persistent_id CHAR(40) NOT NULL, + user_uuid CHAR(36) NOT NULL, + service_provider_uuid CHAR(36) NOT NULL, + PRIMARY KEY (persistent_id) + ) + SQL); + $this->addSql('CREATE INDEX user_uuid ON saml_persistent_id (user_uuid, service_provider_uuid)'); + $this->addSql("COMMENT ON COLUMN saml_persistent_id.persistent_id IS 'SHA1 of COIN: + user_uuid + service_provider_uuid'"); + + $this->addSql(<<<'SQL' + CREATE TABLE service_provider_uuid ( + uuid CHAR(36) NOT NULL, + service_provider_entity_id VARCHAR(1024) NOT NULL, + PRIMARY KEY (uuid) + ) + SQL); + $this->addSql('CREATE INDEX service_provider_entity_id ON service_provider_uuid (service_provider_entity_id)'); + + $this->addSql(<<<'SQL' + CREATE TABLE sso_provider_roles_eb6 ( + id INT GENERATED BY DEFAULT AS IDENTITY 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 BOOLEAN 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 BOOLEAN 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 BOOLEAN DEFAULT NULL, + single_sign_on_services JSON DEFAULT NULL, + consent_settings TEXT DEFAULT NULL, + shib_md_scopes JSON DEFAULT NULL, + idp_discoveries JSON DEFAULT NULL, + PRIMARY KEY (id) + ) + SQL); + $this->addSql('CREATE INDEX idx_sso_provider_roles_type ON sso_provider_roles_eb6 (type)'); + $this->addSql('CREATE INDEX idx_sso_provider_roles_entity_id ON sso_provider_roles_eb6 (entity_id)'); + $this->addSql('CREATE UNIQUE INDEX idx_sso_provider_roles_entity_id_type ON sso_provider_roles_eb6 (type, entity_id)'); + + $this->addSql(<<<'SQL' + CREATE TABLE "user" ( + collab_person_id VARCHAR(255) NOT NULL, + uuid CHAR(36) NOT NULL, + PRIMARY KEY (collab_person_id) + ) + SQL); + $this->addSql("COMMENT ON COLUMN \"user\".collab_person_id IS '(DC2Type:engineblock_collab_person_id)'"); + $this->addSql("COMMENT ON COLUMN \"user\".uuid IS '(DC2Type:engineblock_collab_person_uuid)'"); + $this->addSql('CREATE UNIQUE INDEX uq_user_uuid ON "user" (uuid)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE consent'); + $this->addSql('DROP TABLE saml_persistent_id'); + $this->addSql('DROP TABLE service_provider_uuid'); + $this->addSql('DROP TABLE sso_provider_roles_eb6'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/postgres_support.md b/postgres_support.md new file mode 100644 index 000000000..ce611f12e --- /dev/null +++ b/postgres_support.md @@ -0,0 +1,180 @@ +# Database Backend Support + +EngineBlock can be configured to use either **MariaDB** or **PostgreSQL** as its database backend. + +The database backend is selected through the database configuration, allowing the same EngineBlock setup to run against either supported database without changing the Doctrine connection configuration. + +## Supported Databases + +Other versions will most likely work, but these are the versions that have been tested. + +| Database | Driver | Version | +| ---------- | ----------- | ------------------ | +| MariaDB | `pdo_mysql` | `10.11.13-MariaDB` | +| PostgreSQL | `pdo_pgsql` | `17` | + +## Selecting a Database + +The database connection is configured using the following parameters: + +```yaml +parameters: + database.driver: pdo_pgsql + database.server_version: '17' + database.host: postgres + database.port: '5432' + database.user: ebrw + database.password: secret + database.dbname: eb +``` + +For MariaDB, use: + +```yaml +parameters: + database.driver: pdo_mysql + database.server_version: 10.11.13-MariaDB + database.host: mariadb + database.port: '3306' + database.user: ebrw + database.password: secret + database.dbname: eb +``` + +The Doctrine configuration uses these parameters for the connection: + +```yaml +doctrine: + dbal: + default_connection: engineblock + connections: + engineblock: + driver: "%database.driver%" + dbname: "%database.dbname%" + host: "%database.host%" + port: "%database.port%" + user: "%database.user%" + password: "%database.password%" + server_version: "%database.server_version%" +``` + +This means that changing the database backend only requires changing the database parameters; the Doctrine connection itself remains the same. + + +## PostgreSQL Support + +PostgreSQL support requires the PHP PostgreSQL PDO extension to be installed in the container. + +Install the required PostgreSQL development libraries and PHP extension: + +```bash +apt-get update +apt-get install -y libpq-dev +docker-php-ext-install pdo_pgsql +service apache2 reload +``` + +This adds the `pdo_pgsql` driver required by Doctrine to connect to PostgreSQL. + +The PostgreSQL-specific container setup is therefore an additional requirement when running EngineBlock with PostgreSQL. + +## Database Migrations + +Database migrations are separated into: +```text +migrations/ +├── MariaMigrations/ +└── PostgresMigrations/ +``` + +To run migrations for mariadb use; +```shell +./bin/console doctrine:migrations:migrate +``` + +To run migrations for postgresql use; +```shell +APP_ENV=postgres ./bin/console doctrine:migrations:migrate +``` +To run migrations using postgres in other env you will have to copy [config/packages/postgres/doctrine_migrations.yaml](config/packages/postgres/doctrine_migrations.yaml) +to the required env folder. + + +For MariaDB, it uses the MariaMigrations folder: [doctrine_migrations.yaml](config/packages/doctrine_migrations.yaml) +```yaml +doctrine_migrations: + migrations_paths: + OpenConext\EngineBlock\Doctrine\Migrations: + '%kernel.project_dir%/migrations/MariaMigrations' + storage: + table_storage: + table_name: 'migration_versions' + +``` +For PostgresSQL, it uses the PostgresMigrations folder: [doctrine_migrations.yaml](config/packages/postgres/doctrine_migrations.yaml) +```yaml +doctrine_migrations: + migrations_paths: + OpenConext\EngineBlock\Doctrine\Migrations: + '%kernel.project_dir%/migrations/PostgresMigrations' + OpenConext\EngineBlock\Doctrine\SharedMigrations: + '%kernel.project_dir%/migrations/SharedMigrations' +``` +## How It Works + +The database selection follows this flow: + +```text +Database configuration + │ + ▼ +database.driver + │ + ├── pdo_mysql ──► MariaDB + │ + └── pdo_pgsql ──► PostgreSQL + │ + ▼ + Doctrine DBAL + │ + ▼ + EngineBlock +``` + +The application therefore does not need separate Doctrine connection configurations for each database. The driver, server version and connection details are provided through parameters and consumed by the existing Doctrine configuration. + +## Behat tests + +To run the behat tests with PostgreSQL, you will need to adjust some configuration. + +In [behat.sh](ci/qa/behat.sh) you will need to adjust the migrations running: +```shell +echo -e "\nInstalling database fixtures...\n" +./bin/console doctrine:schema:drop --force --env=ci +./bin/console doctrine:query:sql "DROP TABLE IF EXISTS sso_provider_roles_eb5" +./bin/console doctrine:query:sql "DROP TABLE IF EXISTS migration_versions" +./bin/console doctrine:migrations:migrate --env=ci --no-interaction +``` +In [ci](config/packages/ci) ensure the correct database connection is used: +```yaml +doctrine_migrations: + migrations_paths: + OpenConext\EngineBlock\Doctrine\Migrations: + '%kernel.project_dir%/migrations/PostgresMigrations' + +parameters: + database.driver: pdo_pgsql + database.server_version: '17' + database.host: postgres + database.port: '5432' + database.user: ebrw + database.password: secret + database.dbname: eb + database.test.driver: pdo_pgsql + database.test.server_version: '17' + database.test.host: postgres + database.test.port: '5432' + database.test.user: eb_testrw + database.test.password: secret + database.test.dbname: eb_test +``` diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php deleted file mode 100644 index 448d9a767..000000000 --- a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRoleEb5.php +++ /dev/null @@ -1,347 +0,0 @@ - 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 6a5725266..6fddacfbb 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssembler.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssembler.php @@ -22,9 +22,7 @@ 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; @@ -665,197 +663,4 @@ 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/IdentityProviderEb5.php b/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php deleted file mode 100644 index 934b82a0b..000000000 --- a/src/OpenConext/EngineBlock/Metadata/Entity/IdentityProviderEb5.php +++ /dev/null @@ -1,371 +0,0 @@ - - */ - #[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/ServiceProviderEb5.php b/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php deleted file mode 100644 index eb104fb9f..000000000 --- a/src/OpenConext/EngineBlock/Metadata/Entity/ServiceProviderEb5.php +++ /dev/null @@ -1,416 +0,0 @@ -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/MetadataRepository/DoctrineMetadataPushRepository.php b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php index b98dab047..6c77c40ee 100644 --- a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php +++ b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php @@ -25,48 +25,23 @@ 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") + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DoctrineMetadataPushRepository { - /** - * @var Connection - */ - private $connection; - - /** - * @var ClassMetadata - */ - private $spMetadata; + private Connection $connection; - /** - * @var ClassMetadata - */ - private $idpMetadata; - - /** - * @Deprecated - * @var ClassMetadata - */ - private $spMetadataDeprecated; + private ClassMetadata $spMetadata; - /** - * @Deprecated - * @var ClassMetadata - */ - private $idpMetadataDeprecated; + private ClassMetadata $idpMetadata; - const FIELD_VALUE = 0; - const FIELD_TYPE = 1; + const int FIELD_VALUE = 0; + const int FIELD_TYPE = 1; public function __construct( EntityManager $entityManager, @@ -75,10 +50,6 @@ public function __construct( $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); } /** @@ -96,12 +67,10 @@ public function __construct( * @param AbstractRole[] $roles * @return SynchronizationResult * @throws \Exception + * @throws \Throwable */ - public function synchronize(array $roles, array $rolesEb5): SynchronizationResult + public function synchronize(array $roles): 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); @@ -168,15 +137,10 @@ public function synchronize(array $roles, array $rolesEb5): SynchronizationResul return $result; } - private function insertRole(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $metadata): void + private function insertRole(AbstractRole $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($tableName); + ->insert(AbstractRole::TABLE_NAME); $normalized = $this->addInsertQueryParameters($role, $query, $metadata); $stmt = $this->connection->prepare($query->getSQL()); @@ -184,16 +148,10 @@ private function insertRole(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $m $stmt->executeQuery(); } - private function updateRole(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $metadata): void + private function updateRole(AbstractRole $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() - ->update($tableName); + ->update(AbstractRole::TABLE_NAME); $normalized = $this->addUpdateQueryParameters($role, $query, $metadata); $stmt = $this->connection->prepare($query->getSQL()); @@ -203,14 +161,8 @@ private function updateRole(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $m 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($tableName) + ->delete(AbstractRole::TABLE_NAME) ->where('id IN (:ids)') ->setParameter('ids', $roles, ArrayParameterType::INTEGER); @@ -222,15 +174,9 @@ private function deleteRolesByIds(array $roles, ClassMetadata $metadata): int|st 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($tableName); + ->from(AbstractRole::TABLE_NAME); assert($query instanceof QueryBuilder); $this->addDiscriminatorQuery($query, $metadata); @@ -245,7 +191,7 @@ private function findAllRoleEntityIds(ClassMetadata $metadata): array|null return $results; } - private function addInsertQueryParameters(AbstractRole|AbstractRoleEb5 $role, QueryBuilder $query, ClassMetadata $metadata): array + private function addInsertQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata): array { $normalized = $this->normalizeData($role, $metadata); foreach (array_keys($normalized) as $id) { @@ -254,7 +200,7 @@ private function addInsertQueryParameters(AbstractRole|AbstractRoleEb5 $role, Qu return $normalized; } - private function addUpdateQueryParameters(AbstractRole|AbstractRoleEb5 $role, QueryBuilder $query, ClassMetadata $metadata): array + private function addUpdateQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata): array { $normalized = $this->normalizeData($role, $metadata); foreach (array_keys($normalized) as $id) { @@ -280,7 +226,7 @@ private function addDiscriminatorQuery(QueryBuilder $queryBuilder, ClassMetadata ->setParameter($metadata->discriminatorColumn->name, $metadata->discriminatorValue, $metadata->discriminatorColumn->type); } - private function normalizeData(AbstractRole|AbstractRoleEb5 $role, ClassMetadata $metadata): array + private function normalizeData(AbstractRole $role, ClassMetadata $metadata): array { $result = []; foreach ($metadata->fieldMappings as $id => $columnInfo) { @@ -300,78 +246,4 @@ private function normalizeData(AbstractRole|AbstractRoleEb5 $role, ClassMetadata 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/EngineBlockBundle/Authentication/Entity/User.php b/src/OpenConext/EngineBlockBundle/Authentication/Entity/User.php index d5b6ff76a..61335c407 100644 --- a/src/OpenConext/EngineBlockBundle/Authentication/Entity/User.php +++ b/src/OpenConext/EngineBlockBundle/Authentication/Entity/User.php @@ -19,24 +19,22 @@ namespace OpenConext\EngineBlockBundle\Authentication\Entity; use Doctrine\ORM\Mapping as ORM; +use OpenConext\EngineBlock\Authentication\Value\CollabPersonId; use OpenConext\EngineBlock\Authentication\Value\CollabPersonUuid; use OpenConext\EngineBlockBundle\Authentication\Repository\UserRepository; +/** + * User is a reserved word in PSQL and thus needs to be escaped; when adjusted, we should consider a different table name + */ #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\UniqueConstraint(name: 'uq_user_uuid', columns: ['uuid'])] +#[ORM\Table(name: '`user`')] class User { - /** - * @var - */ #[ORM\Id] - #[ORM\Column(type: 'engineblock_collab_person_id')] - public $collabPersonId; + #[ORM\Column(name: 'collab_person_id', type: 'engineblock_collab_person_id')] + public CollabPersonId $collabPersonId; - /** - * @var CollabPersonUuid - * - */ #[ORM\Column(name: 'uuid', type: 'engineblock_collab_person_uuid')] - public $collabPersonUuid; + public CollabPersonUuid $collabPersonUuid; } diff --git a/src/OpenConext/EngineBlockBundle/Authentication/Repository/DbalConsentRepository.php b/src/OpenConext/EngineBlockBundle/Authentication/Repository/DbalConsentRepository.php index 64ac008cf..a65799123 100644 --- a/src/OpenConext/EngineBlockBundle/Authentication/Repository/DbalConsentRepository.php +++ b/src/OpenConext/EngineBlockBundle/Authentication/Repository/DbalConsentRepository.php @@ -225,6 +225,35 @@ public function hasConsentHash(ConsentHashQuery $query): ConsentVersion * @throws RuntimeException */ public function storeConsentHash(ConsentStoreParameters $parameters): bool + { + $driver = $this->connection->getParams()['driver'] ?? null; + + try { + switch ($driver) { + case 'pdo_mysql': + return $this->storeConsentHashMariaDb($parameters); + + case 'pdo_pgsql': + return $this->storeConsentHashPostgres($parameters); + + default: + throw new RuntimeException( + sprintf('Unsupported database driver: "%s"', $driver) + ); + } + } catch (Exception $e) { + throw new RuntimeException( + sprintf('Error storing consent: "%s"', $e->getMessage()), + 0, + $e + ); + } + } + + /** + * @throws RuntimeException + */ + public function storeConsentHashMariaDb(ConsentStoreParameters $parameters): bool { $query = "INSERT INTO consent (hashed_user_id, service_id, attribute, attribute_stable, consent_type, consent_date, deleted_at) VALUES (?, ?, ?, ?, ?, NOW(), '0000-00-00 00:00:00') @@ -237,12 +266,56 @@ public function storeConsentHash(ConsentStoreParameters $parameters): bool $parameters->attributeStableHash, $parameters->consentType, ]; + $this->connection->executeStatement($query, $bindings); - try { - $this->connection->executeStatement($query, $bindings); - } catch (Exception $e) { - throw new RuntimeException( - sprintf('Error storing consent: "%s"', $e->getMessage()) + return true; + } + + /** + * See Consent.php for more information on the deleted_at column. However, + * since (by legacy?) consent has primary key on deleted_at it cannot be null + * + * @throws Exception + */ + private function storeConsentHashPostgres(ConsentStoreParameters $parameters): bool + { + $conn = $this->connection; + + $exists = $conn->fetchOne( + "SELECT 1 + FROM consent + WHERE hashed_user_id = :hashedUserId + AND service_id = :serviceId + AND deleted_at = '1970-01-01 00:00:00'", + [ + 'hashedUserId' => $parameters->hashedUserId, + 'serviceId' => $parameters->serviceId, + ] + ); + if ($exists) { + $conn->executeStatement( + 'UPDATE consent SET consent_date = NOW(), attribute = :attribute, attribute_stable = :attributeStable, consent_type = :consentType + WHERE hashed_user_id = :hashedUserId AND service_id = :serviceId AND deleted_at = :deletedAt', + [ + 'attribute' => $parameters->attributeHash, + 'attributeStable' => $parameters->attributeStableHash, + 'consentType' => $parameters->consentType, + 'hashedUserId' => $exists->hashedUserId, + 'serviceId' => $exists->serviceId, + 'deletedAt' => $exists->deleted_at, + ] + ); + } else { + $conn->executeStatement( + "INSERT INTO consent (consent_date, hashed_user_id, service_id, attribute, attribute_stable, consent_type, deleted_at) + VALUES (NOW(), :hashedUserId, :serviceId, :attribute, :attributeStable, :consentType, '1970-01-01 00:00:00')", + [ + 'hashedUserId' => $parameters->hashedUserId, + 'serviceId' => $parameters->serviceId, + 'attribute' => $parameters->attributeHash, + 'attributeStable' => $parameters->attributeStableHash, + 'consentType' => $parameters->consentType, + ] ); } diff --git a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php index 07f08a5cf..e90f795bf 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsController.php @@ -122,8 +122,6 @@ 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); } @@ -131,7 +129,7 @@ public function pushConnectionsAction(Request $request) unset($body); try { - $result = $this->repository->synchronize($roles, $rolesEb5); + $result = $this->repository->synchronize($roles); } catch (Exception $exception) { throw new ApiInternalServerErrorHttpException('Unable to synchronize the assembled roles to the repository', $exception); } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php index ccfba5d54..53bb9ee4c 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php @@ -165,10 +165,10 @@ public function registerSp($name, $entityId, $acsLocation, $certData = '') // number of SP's should always be limited. $tableName = AbstractRole::TABLE_NAME; $idpEntityIDQuery = <<entityManager->getConnection()->prepare($idpEntityIDQuery); assert($query instanceof Statement); $result = $query->executeQuery(); @@ -203,10 +203,10 @@ public function registerIdp($name, $entityId, $ssoLocation, $certData = '', $dis // number of SP's should always be limited. $tableName = AbstractRole::TABLE_NAME; $spEntityIDQuery = <<entityManager->getConnection()->prepare($spEntityIDQuery); assert($query instanceof Statement); $result = $query->executeQuery(); From 8617431473ebf914aed571d78964250b562d4894 Mon Sep 17 00:00:00 2001 From: Stephan Kok Date: Mon, 24 Aug 2026 14:17:42 +0200 Subject: [PATCH 7/7] Add postgres tests to ci/cd --- .github/workflows/test-integration.yml | 14 ++++++++++++++ config/bundles.php | 8 ++++---- config/packages/postgres/doctrine.yaml | 12 ++++++++++++ config/packages/postgres/framework.yaml | 9 +++++++++ config/packages/postgres/monolog.yaml | 7 +++++++ config/packages/postgres/parameters.yml | 13 +++++++++++++ config/packages/postgres/web_profiler.yaml | 6 ++++++ config/reference.php | 14 ++++++++++++++ docker/ci/Dockerfile-php85 | 4 ++++ docker/docker-compose.yml | 14 ++++++++++++++ 10 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 config/packages/postgres/doctrine.yaml create mode 100644 config/packages/postgres/framework.yaml create mode 100644 config/packages/postgres/monolog.yaml create mode 100644 config/packages/postgres/web_profiler.yaml diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 0daa99d87..1b35c1013 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -78,6 +78,20 @@ jobs: echo -e "\nBehat tests (with headless Chrome)\n" && \ ./vendor/bin/behat -c ./tests/behat.yml --suite functional -vv --format pretty --strict ' + - name: Run acceptance tests - PostgreSQL + if: always() + run: | + cd docker && docker compose exec -e APP_ENV=postgres -T --user www-data engine.dev.openconext.local bash -c ' + echo -e "\nInstalling database fixtures...\n" && \ + ./bin/console doctrine:schema:drop --force --env=postgres && \ + ./bin/console doctrine:migrations:migrate --env=postgres --no-interaction && \ + echo -e "\nPreparing frontend assets\n" && \ + EB_THEME=skeune ./theme/scripts/prepare-test.js > /dev/null && \ + echo -e "\nRun the Behat tests\n" && \ + ./vendor/bin/behat -c ./tests/behat.yml --suite default -vv --format pretty --strict && \ + echo -e "\nBehat tests (with headless Chrome)\n" && \ + ./vendor/bin/behat -c ./tests/behat.yml --suite functional -vv --format pretty --strict + ' - name: Run linting tests if: always() run: | diff --git a/config/bundles.php b/config/bundles.php index c4df50a8e..491b878e5 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -10,8 +10,8 @@ OpenConext\MonitorBundle\OpenConextMonitorBundle::class => ['all' => true], OpenConext\EngineBlockBundle\OpenConextEngineBlockBundle::class => ['all' => true], Liip\FunctionalTestBundle\LiipFunctionalTestBundle::class => ['test' => true], - Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'ci' => true], - Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'ci' => true], - OpenConext\EngineBlockFunctionalTestingBundle\OpenConextEngineBlockFunctionalTestingBundle::class => ['test' => true, 'ci' => true], - FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['ci' => true], + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'ci' => true, 'postgres' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'ci' => true, 'postgres' => true], + OpenConext\EngineBlockFunctionalTestingBundle\OpenConextEngineBlockFunctionalTestingBundle::class => ['test' => true, 'ci' => true, 'postgres' => true], + FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['ci' => true, 'postgres' => true], ]; diff --git a/config/packages/postgres/doctrine.yaml b/config/packages/postgres/doctrine.yaml new file mode 100644 index 000000000..7993c21f3 --- /dev/null +++ b/config/packages/postgres/doctrine.yaml @@ -0,0 +1,12 @@ +doctrine: + dbal: + default_connection: engineblock_test + connections: + engineblock_test: + driver: "%database.test.driver%" + server_version: "%database.test.server_version%" + dbname: "%database.test.dbname%" + host: "%database.test.host%" + port: "%database.test.port%" + user: "%database.test.user%" + password: "%database.test.password%" diff --git a/config/packages/postgres/framework.yaml b/config/packages/postgres/framework.yaml new file mode 100644 index 000000000..2e6cf7109 --- /dev/null +++ b/config/packages/postgres/framework.yaml @@ -0,0 +1,9 @@ +framework: + test: true + +parameters: + router.request_context.host: engine.%domain% + router.request_context.scheme: https + idp_fixture_file: /tmp/eb-fixtures/db/idp.states.php.serialized + sp_fixture_file: /tmp/eb-fixtures/db/sp.states.php.serialized + stepup.sfo.override_engine_entityid: 'https://engine.dev.openconext.local/new/stepup/metadata' diff --git a/config/packages/postgres/monolog.yaml b/config/packages/postgres/monolog.yaml new file mode 100644 index 000000000..eb8d0dc3d --- /dev/null +++ b/config/packages/postgres/monolog.yaml @@ -0,0 +1,7 @@ +monolog: + handlers: + test_log_file: + type: stream + path: '/tmp/eb-fixtures/log-records.ndjson' + level: debug + formatter: monolog.formatter.json diff --git a/config/packages/postgres/parameters.yml b/config/packages/postgres/parameters.yml index 8c3bdc272..0f9f399c1 100644 --- a/config/packages/postgres/parameters.yml +++ b/config/packages/postgres/parameters.yml @@ -1,4 +1,17 @@ parameters: + api.users.nameidlookup.username: nameid + api.users.nameidlookup.password: secret + feature_api_users_nameid_lookup: true + feature_hide_bookmarkable_url: true + auth.log.attributes: + uid: 'urn:mace:dir:attribute-def:uid' + encryption_keys: + default: + publicFile: /config/engine/engineblock.crt + privateFile: /config/engine/engineblock.pem + rollover: + publicFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.crt' + privateFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.key' database.driver: pdo_pgsql database.server_version: '17' database.host: postgres diff --git a/config/packages/postgres/web_profiler.yaml b/config/packages/postgres/web_profiler.yaml new file mode 100644 index 000000000..03752de21 --- /dev/null +++ b/config/packages/postgres/web_profiler.yaml @@ -0,0 +1,6 @@ +web_profiler: + toolbar: false + intercept_redirects: false + +framework: + profiler: { collect: false } diff --git a/config/reference.php b/config/reference.php index dcb968430..592241d3f 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1494,6 +1494,19 @@ * debug?: DebugConfig, * web_profiler?: WebProfilerConfig, * }, + * "when@postgres"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * security?: SecurityConfig, + * twig?: TwigConfig, + * monolog?: MonologConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * debug?: DebugConfig, + * web_profiler?: WebProfilerConfig, + * }, * "when@test"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1592,6 +1605,7 @@ public static function config(array $config): array * @psalm-type RoutesConfig = array{ * "when@ci"?: array, * "when@dev"?: array, + * "when@postgres"?: array, * "when@test"?: array, * ... * } diff --git a/docker/ci/Dockerfile-php85 b/docker/ci/Dockerfile-php85 index e5841012d..a7f6a19d4 100644 --- a/docker/ci/Dockerfile-php85 +++ b/docker/ci/Dockerfile-php85 @@ -2,6 +2,10 @@ FROM ghcr.io/openconext/openconext-basecontainers/php85-apache2-node24:latest RUN a2enmod ssl +# Add postgres driver +RUN apt-get update +RUN apt-get install -y libpq-dev +RUN docker-php-ext-install pdo_pgsql COPY docker/ci/app.ini /usr/local/etc/php/conf.d/ COPY docker/ci/apache2.conf /etc/apache2/sites-enabled/ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index dc5d53f4f..d33d5058e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -17,6 +17,19 @@ services: retries: 20 interval: 2s + postgres: + image: postgres:17 + container_name: eb-psql-db-test + restart: unless-stopped + environment: + POSTGRES_DB: "eb_test" + POSTGRES_USER: "eb_testrw" + POSTGRES_PASSWORD: "secret" + ports: + - "5432:5432" + volumes: + - eb-psql-test-data:/var/lib/postgresql/data + engine.dev.openconext.local: build: context: ../ @@ -62,3 +75,4 @@ services: volumes: eb-mysql-test-data: + eb-psql-test-data: