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 3277a46df5..d7afffb175 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 e85a85b6ed..00fb1a1baf 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 b02340fed5..3f86433902 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 a2e1c5563d..ae79f2b605 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 c7a371fb94..dcb9684303 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 0000000000..f38e100430 --- /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 62723b0d3f..c5d9d1a4e7 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 0000000000..448d9a7676 --- /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 6fddacfbb8..6a57252667 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 0000000000..934b82a0b7 --- /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 0000000000..eb104fb9fb --- /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 5ee90ecc2a..4f5a595f17 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 e90f795bfe..e27facf8ed 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 0000000000..f6f2b48b6d --- /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 caccd0dbc4..ccfba5d54b 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 1766d79ea3..12d8535d18 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 73dcb4e8a6..4d7503db78 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 525d4d5b86..30b81ece30 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 76ffe63304..20aaa5cec3 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 0000000000..64ad203f6e --- /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 ae79f2b605..ceb365aecc 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 c202b79376..4fd89e4b9c 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 9f49cd4871..0d0431b7d5 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 c5d9d1a4e7..226206ee83 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 75d1846f4b..188e660153 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 7d81229c2b..5b994e71bc 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 c1acb6f9c4..f7666e67af 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 f6def40041..25238691c8 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 ae9d5ef911..0b491d5a56 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 ec4865748e..da31caf882 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 925062a212..77b06a0a75 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 b254c7cc99..72b88943a0 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 3bf21c1426..431f50a54a 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 a9073cdb0c..e8710435d2 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 fb7abf5c78..d9c074ebe7 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 400bb5da12..a80d4faf95 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 e1c5402ab1..8eb426bcaf 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 0000000000..8b49b8e9cc --- /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 0000000000..0afdf1921f --- /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 0000000000..a92dbba8f7 --- /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 0000000000..3cfb80359a --- /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 0000000000..c08d7d6664 --- /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 0000000000..58b0e9df6b --- /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 0000000000..4b15f396d5 --- /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 0000000000..b3833b04d3 --- /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 0000000000..691e867a2f --- /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 0000000000..900878d9d5 --- /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 0000000000..ee06baef52 --- /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 0000000000..44cd44a8fb --- /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 0000000000..6ae127e363 --- /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 0000000000..7b9cf392ee --- /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 0000000000..915af93f78 --- /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 0000000000..036aa2d7d2 --- /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 0000000000..0777d8d556 --- /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 0000000000..a1ae33d360 --- /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 f38e100430..41af236981 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 e8710435d2..96c7726449 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 c08d7d6664..97a1d3bac1 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 7b9cf392ee..8a4e940cf7 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 0d0431b7d5..ea7bcf5930 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 72b88943a0..a63cba09e7 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 4f5a595f17..b98dab0478 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 e27facf8ed..07f08a5cf5 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 f6f2b48b6d..f0cd590ef7 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 900878d9d5..c9548d46d3 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 64ad203f6e..79e858553d 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 ee06baef52..270ad309dc 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 44cd44a8fb..40dedb683a 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 6ae127e363..be79b26a11 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 8a4e940cf7..5b0f14770b 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 915af93f78..571130ae1f 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 036aa2d7d2..b66cb93089 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 0777d8d556..25e9e121b7 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 a1ae33d360..46009422e9 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 1dbb587bab..c88801f295 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 26f9ce1f98..97692e383e 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 b6faf85f6d..a0d49ceb86 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 86a61fcf8f..020dc619e1 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 ceb365aecc..7882bda655 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 0c232efcbf..8a87edf490 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 18212e78e5..c3774a0faf 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 0000000000..3583e0dbca --- /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 0000000000..8c3bdc2720 --- /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 448b48047c..9759d3392d 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 13bb3bc86e..dc5d53f4f9 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 bd047836b1..587ce772a6 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 0000000000..4a04554eac --- /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 0000000000..f10642b9bf --- /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 0000000000..ce611f12eb --- /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 448d9a7676..0000000000 --- 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 6a57252667..6fddacfbb8 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 934b82a0b7..0000000000 --- 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 eb104fb9fb..0000000000 --- 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 b98dab0478..6c77c40ee8 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 d5b6ff76a9..61335c4070 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 64ac008cf6..a657991230 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 07f08a5cf5..e90f795bfe 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 ccfba5d54b..53bb9ee4ce 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 0daa99d87d..1b35c10132 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 c4df50a8ee..491b878e5a 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 0000000000..7993c21f30 --- /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 0000000000..2e6cf71090 --- /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 0000000000..eb8d0dc3d8 --- /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 8c3bdc2720..0f9f399c1e 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 0000000000..03752de213 --- /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 dcb9684303..592241d3fa 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 e5841012de..a7f6a19d4e 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 dc5d53f4f9..d33d5058e5 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: