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/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/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/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/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/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 a2e1c5563d..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: @@ -23,6 +23,16 @@ doctrine: engineblock_collab_person_uuid: OpenConext\EngineBlockBundle\Doctrine\Type\CollabPersonUuidType engineblock_metadata_coins: OpenConext\EngineBlockBundle\Doctrine\Type\MetadataCoinType engineblock_metadata_mdui: OpenConext\EngineBlockBundle\Doctrine\Type\MetadataMduiType + engineblock_certificate_array: OpenConext\EngineBlockBundle\Doctrine\Type\CertificateArrayType + engineblock_attribute_release_policy: OpenConext\EngineBlockBundle\Doctrine\Type\AttributeReleasePolicyType + engineblock_contact_person_array: OpenConext\EngineBlockBundle\Doctrine\Type\ContactPersonArrayType + engineblock_indexed_service_array: OpenConext\EngineBlockBundle\Doctrine\Type\IndexedServiceArrayType + engineblock_logo: OpenConext\EngineBlockBundle\Doctrine\Type\LogoType + engineblock_organization: OpenConext\EngineBlockBundle\Doctrine\Type\OrganizationType + engineblock_requested_attribute_array: OpenConext\EngineBlockBundle\Doctrine\Type\RequestedAttributeArrayType + engineblock_service: OpenConext\EngineBlockBundle\Doctrine\Type\ServiceType + engineblock_service_array: OpenConext\EngineBlockBundle\Doctrine\Type\ServiceArrayType + engineblock_shib_md_scope_array: OpenConext\EngineBlockBundle\Doctrine\Type\ShibMdScopeArrayType array: OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType object: OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType diff --git a/config/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.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/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/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 new file mode 100644 index 0000000000..0f9f399c1e --- /dev/null +++ b/config/packages/postgres/parameters.yml @@ -0,0 +1,28 @@ +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 + 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/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/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/config/reference.php b/config/reference.php index c7a371fb94..592241d3fa 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,7 +1494,7 @@ * debug?: DebugConfig, * web_profiler?: WebProfilerConfig, * }, - * "when@prod"?: array{ + * "when@postgres"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, @@ -1502,6 +1504,8 @@ * monolog?: MonologConfig, * doctrine?: DoctrineConfig, * doctrine_migrations?: DoctrineMigrationsConfig, + * debug?: DebugConfig, + * web_profiler?: WebProfilerConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -1601,7 +1605,7 @@ public static function config(array $config): array * @psalm-type RoutesConfig = array{ * "when@ci"?: array, * "when@dev"?: array, - * "when@prod"?: 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 13bb3bc86e..d33d5058e5 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" @@ -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: 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 95% rename from migrations/DoctrineMigrations/Version20260210000000.php rename to migrations/MariaMigrations/Version20260210000000.php index 3277a46df5..d7afffb175 100644 --- a/migrations/DoctrineMigrations/Version20260210000000.php +++ b/migrations/MariaMigrations/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/MariaMigrations/Version20260224000000.php similarity index 82% rename from migrations/DoctrineMigrations/Version20260224000000.php rename to migrations/MariaMigrations/Version20260224000000.php index e85a85b6ed..00fb1a1baf 100644 --- a/migrations/DoctrineMigrations/Version20260224000000.php +++ b/migrations/MariaMigrations/Version20260224000000.php @@ -40,11 +40,17 @@ public function getDescription(): string return 'Patch migration: Removes the deleted_at index from the consent table. Skips if the index does not exist.'; } - public function preUp(Schema $schema): void + public function up(Schema $schema): void { - parent::preUp($schema); + $indexes = $this->connection + ->createSchemaManager() + ->introspectTableIndexes( + new OptionallyQualifiedName( + Identifier::unquoted('consent'), + null + ) + ); - $indexes = $this->connection->createSchemaManager()->introspectTableIndexes(new OptionallyQualifiedName(Identifier::unquoted('consent'), null)); $deletedAtIndex = array_filter( $indexes, static fn(Index $index) => $index->getObjectName()->equals( @@ -53,14 +59,11 @@ public function preUp(Schema $schema): void ) ); - $this->skipIf( - count($deletedAtIndex) === 0, - 'Index deleted_at on consent table does not exist. Skipping.' - ); - } + // Index deleted_at on consent table does not exist. Skipping + if (count($deletedAtIndex) === 0) { + return; + } - public function up(Schema $schema): void - { $this->addSql('ALTER TABLE `consent` DROP INDEX `deleted_at`'); } diff --git a/migrations/DoctrineMigrations/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/MariaMigrations/Version20260817133323.php b/migrations/MariaMigrations/Version20260817133323.php new file mode 100644 index 0000000000..41af236981 --- /dev/null +++ b/migrations/MariaMigrations/Version20260817133323.php @@ -0,0 +1,80 @@ +addSql(<<<'SQL' + CREATE TABLE sso_provider_roles_eb6 ( + id INT AUTO_INCREMENT NOT NULL, + entity_id VARCHAR(255) NOT NULL, + name_nl VARCHAR(255), + name_en VARCHAR(255), + name_pt VARCHAR(255), + description_nl VARCHAR(255), + description_en VARCHAR(255), + description_pt VARCHAR(255), + display_name_nl VARCHAR(255), + display_name_en VARCHAR(255), + display_name_pt VARCHAR(255), + logo JSON, + organization_nl_name JSON DEFAULT NULL, + organization_en_name JSON DEFAULT NULL, + organization_pt_name JSON DEFAULT NULL, + keywords_nl VARCHAR(255), + keywords_en VARCHAR(255), + keywords_pt VARCHAR(255), + certificates JSON, + workflow_state VARCHAR(255) NOT NULL, + contact_persons JSON, + name_id_format VARCHAR(255) DEFAULT NULL, + name_id_formats JSON NOT NULL, + single_logout_service JSON DEFAULT NULL, + requests_must_be_signed TINYINT NOT NULL, + manipulation TEXT, + coins JSON NOT NULL, + mdui JSON NOT NULL, + type VARCHAR(255) NOT NULL, + attribute_release_policy JSON DEFAULT NULL, + assertion_consumer_services JSON DEFAULT NULL, + allowed_idp_entity_ids JSON DEFAULT NULL, + allow_all TINYINT DEFAULT NULL, + requested_attributes JSON DEFAULT NULL, + support_url_en VARCHAR(255) DEFAULT NULL, + support_url_nl VARCHAR(255) DEFAULT NULL, + support_url_pt VARCHAR(255) DEFAULT NULL, + enabled_in_wayf TINYINT DEFAULT NULL, + single_sign_on_services JSON DEFAULT NULL, + consent_settings LONGTEXT DEFAULT NULL, + shib_md_scopes JSON DEFAULT NULL, + idp_discoveries JSON DEFAULT NULL, + INDEX idx_sso_provider_roles_type (type), + INDEX idx_sso_provider_roles_entity_id (entity_id), + UNIQUE INDEX idx_sso_provider_roles_entity_id_type (type, entity_id), + PRIMARY KEY (id) + ) DEFAULT CHARACTER SET UTF8 + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE sso_provider_roles_eb6'); + } +} diff --git a/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/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..ea7bcf5930 100644 --- a/src/OpenConext/EngineBlock/Metadata/ContactPerson.php +++ b/src/OpenConext/EngineBlock/Metadata/ContactPerson.php @@ -24,18 +24,31 @@ */ class ContactPerson { - public $contactType; - public $emailAddress = ''; - public $telephoneNumber = ''; - public $givenName = ''; - public $surName = ''; + public string $contactType; + public string $emailAddress; + public string $telephoneNumber; + public string $givenName; + public string $surName; /** * @param $contactType + * @param string $emailAddress + * @param string $telephoneNumber + * @param string $givenName + * @param string $surName */ - public function __construct($contactType) - { + public function __construct( + $contactType, + string $emailAddress = '', + string $telephoneNumber = '', + string $givenName = '', + string $surName = '' + ) { $this->contactType = $contactType; + $this->emailAddress = $emailAddress; + $this->telephoneNumber = $telephoneNumber; + $this->givenName = $givenName; + $this->surName = $surName; } /** @@ -61,4 +74,19 @@ public static function from( $contact->telephoneNumber = $telephoneNumber; return $contact; } + + + /** + * A convenience static constructor for the contact person. + */ + public static function fromArray(array $contactPerson): ContactPerson + { + return new self( + $contactPerson["contactType"], + $contactPerson["emailAddress"], + $contactPerson["telephoneNumber"], + $contactPerson["givenName"], + $contactPerson["surName"] + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php index 62723b0d3f..226206ee83 100644 --- a/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php +++ b/src/OpenConext/EngineBlock/Metadata/Entity/AbstractRole.php @@ -28,8 +28,11 @@ use OpenConext\EngineBlock\Metadata\Organization; use OpenConext\EngineBlock\Metadata\Service; use OpenConext\EngineBlock\Metadata\X509\X509Certificate; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedArrayType; -use OpenConext\EngineBlockBundle\Doctrine\Type\SerializedObjectType; +use OpenConext\EngineBlockBundle\Doctrine\Type\CertificateArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\ContactPersonArrayType; +use OpenConext\EngineBlockBundle\Doctrine\Type\LogoType; +use OpenConext\EngineBlockBundle\Doctrine\Type\OrganizationType; +use OpenConext\EngineBlockBundle\Doctrine\Type\ServiceType; use RuntimeException; use SAML2\Constants; @@ -51,15 +54,16 @@ #[ORM\InheritanceType('SINGLE_TABLE')] #[ORM\DiscriminatorColumn(name: 'type', type: 'string')] #[ORM\DiscriminatorMap(['sp' => ServiceProvider::class, 'idp' => IdentityProvider::class])] -#[ORM\Table(name: 'sso_provider_roles_eb5')] +#[ORM\Table(name: 'sso_provider_roles_eb6')] #[ORM\Index(name: 'idx_sso_provider_roles_type', columns: ['type'])] #[ORM\Index(name: 'idx_sso_provider_roles_entity_id', columns: ['entity_id'])] #[ORM\UniqueConstraint(name: 'idx_sso_provider_roles_entity_id_type', columns: ['type', 'entity_id'])] abstract class AbstractRole { - const WORKFLOW_STATE_PROD = 'prodaccepted'; - const WORKFLOW_STATE_TEST = 'testaccepted'; - const WORKFLOW_STATE_DEFAULT = self::WORKFLOW_STATE_PROD; + const string TABLE_NAME = 'sso_provider_roles_eb6'; + const string WORKFLOW_STATE_PROD = 'prodaccepted'; + const string WORKFLOW_STATE_TEST = 'testaccepted'; + const string WORKFLOW_STATE_DEFAULT = self::WORKFLOW_STATE_PROD; /** * @var int @@ -78,126 +82,126 @@ abstract class AbstractRole /** * @var string */ - #[ORM\Column(name: 'name_nl', type: Types::STRING)] + #[ORM\Column(name: 'name_nl', type: Types::STRING, nullable: true)] public ?string $nameNl = null; /** * @var string */ - #[ORM\Column(name: 'name_en', type: Types::STRING)] + #[ORM\Column(name: 'name_en', type: Types::STRING, nullable: true)] public ?string $nameEn = null; /** * @var string */ - #[ORM\Column(name: 'name_pt', type: Types::STRING)] + #[ORM\Column(name: 'name_pt', type: Types::STRING, nullable: true)] public ?string $namePt = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'description_nl', type: Types::STRING)] + #[ORM\Column(name: 'description_nl', type: Types::STRING, nullable: true)] public ?string $descriptionNl = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'description_en', type: Types::STRING)] + #[ORM\Column(name: 'description_en', type: Types::STRING, nullable: true)] public ?string $descriptionEn = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'description_pt', type: Types::STRING)] + #[ORM\Column(name: 'description_pt', type: Types::STRING, nullable: true)] public ?string $descriptionPt = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'display_name_nl', type: Types::STRING)] + #[ORM\Column(name: 'display_name_nl', type: Types::STRING, nullable: true)] public ?string $displayNameNl = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'display_name_en', type: Types::STRING)] + #[ORM\Column(name: 'display_name_en', type: Types::STRING, nullable: true)] public ?string $displayNameEn = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'display_name_pt', type: Types::STRING)] + #[ORM\Column(name: 'display_name_pt', type: Types::STRING, nullable: true)] public ?string $displayNamePt = null; /** * @var Logo * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'logo', type: SerializedObjectType::NAME)] - public $logo; + #[ORM\Column(name: 'logo', type: LogoType::NAME, nullable: true)] + public ?Logo $logo; /** * @var Organization */ - #[ORM\Column(name: 'organization_nl_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $organizationNl; + #[ORM\Column(name: 'organization_nl_name', type: OrganizationType::NAME, length: 65535, nullable: true)] + public ?Organization $organizationNl; /** * @var Organization */ - #[ORM\Column(name: 'organization_en_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $organizationEn; + #[ORM\Column(name: 'organization_en_name', type: OrganizationType::NAME, length: 65535, nullable: true)] + public ?Organization $organizationEn; /** * @var Organization */ - #[ORM\Column(name: 'organization_pt_name', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $organizationPt; + #[ORM\Column(name: 'organization_pt_name', type: OrganizationType::NAME, length: 65535, nullable: true)] + public ?Organization $organizationPt; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'keywords_nl', type: Types::STRING)] + #[ORM\Column(name: 'keywords_nl', type: Types::STRING, nullable: true)] public ?string $keywordsNl = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'keywords_en', type: Types::STRING)] + #[ORM\Column(name: 'keywords_en', type: Types::STRING, nullable: true)] public ?string $keywordsEn = null; /** * @var string * @deprecated Will be removed in favour of using the Mdui value object, use the getter for this field instead */ - #[ORM\Column(name: 'keywords_pt', type: Types::STRING)] + #[ORM\Column(name: 'keywords_pt', type: Types::STRING, nullable: true)] public ?string $keywordsPt = null; /** * @var X509Certificate[] */ - #[ORM\Column(name: 'certificates', type: SerializedArrayType::NAME, length: 65535)] - public $certificates = array(); + #[ORM\Column(name: 'certificates', type: CertificateArrayType::NAME, length: 65535, nullable: true)] + public array $certificates = array(); /** * @var string */ #[ORM\Column(name: 'workflow_state', type: Types::STRING)] - public ?string $workflowState = self::WORKFLOW_STATE_DEFAULT; + public string $workflowState = self::WORKFLOW_STATE_DEFAULT; /** * @var ContactPerson[] */ - #[ORM\Column(name: 'contact_persons', type: SerializedArrayType::NAME, length: 65535)] - public $contactPersons; + #[ORM\Column(name: 'contact_persons', type: ContactPersonArrayType::NAME, length: 65535, nullable: true)] + public array $contactPersons = array(); /** * @var string @@ -208,14 +212,14 @@ abstract class AbstractRole /** * @var string[] */ - #[ORM\Column(name: 'name_id_formats', type: SerializedArrayType::NAME, length: 65535)] - public $supportedNameIdFormats; + #[ORM\Column(name: 'name_id_formats', type: Types::JSON, length: 65535)] + public array $supportedNameIdFormats; /** * @var Service */ - #[ORM\Column(name: 'single_logout_service', type: SerializedObjectType::NAME, length: 65535, nullable: true)] - public $singleLogoutService; + #[ORM\Column(name: 'single_logout_service', type: ServiceType::NAME, length: 65535, nullable: true)] + public ?Service $singleLogoutService; /** * @var bool @@ -226,20 +230,20 @@ abstract class AbstractRole /** * @var string */ - #[ORM\Column(name: 'manipulation', type: Types::TEXT, length: 65535)] + #[ORM\Column(name: 'manipulation', type: Types::TEXT, length: 65535, nullable: true)] public ?string $manipulation = null; /** * @var Coins */ #[ORM\Column(name: 'coins', type: 'engineblock_metadata_coins')] - protected $coins = array(); + protected Coins $coins; /** * @var Mdui */ #[ORM\Column(name: 'mdui', type: 'engineblock_metadata_mdui')] - protected $mdui; + protected Mdui $mdui; public function __construct( $entityId, @@ -250,19 +254,19 @@ public function __construct( ?Service $singleLogoutService = null, array $certificates = array(), array $contactPersons = array(), - ?string $descriptionEn = '', - ?string $descriptionNl = '', - ?string $descriptionPt = '', - ?string $displayNameEn = '', - ?string $displayNameNl = '', - ?string $displayNamePt = '', - ?string $keywordsEn = '', - ?string $keywordsNl = '', - ?string $keywordsPt = '', + ?string $descriptionEn = null, + ?string $descriptionNl = null, + ?string $descriptionPt = null, + ?string $displayNameEn = null, + ?string $displayNameNl = null, + ?string $displayNamePt = null, + ?string $keywordsEn = null, + ?string $keywordsNl = null, + ?string $keywordsPt = null, ?Logo $logo = null, - ?string $nameEn = '', - ?string $nameNl = '', - ?string $namePt = '', + ?string $nameEn = null, + ?string $nameNl = null, + ?string $namePt = null, ?string $nameIdFormat = null, array $supportedNameIdFormats = array( Constants::NAMEID_TRANSIENT, @@ -270,7 +274,7 @@ public function __construct( ), bool $requestsMustBeSigned = false, string $workflowState = self::WORKFLOW_STATE_DEFAULT, - string $manipulation = '' + ?string $manipulation = null ) { $this->mdui = $mdui; $this->certificates = $certificates; @@ -306,10 +310,7 @@ public function __construct( */ abstract public function accept(VisitorInterface $visitor); - /** - * @return string - */ - public function getManipulation() + public function getManipulation(): ?string { return $this->manipulation; } diff --git a/src/OpenConext/EngineBlock/Metadata/Entity/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..a63cba09e7 100644 --- a/src/OpenConext/EngineBlock/Metadata/IndexedService.php +++ b/src/OpenConext/EngineBlock/Metadata/IndexedService.php @@ -24,29 +24,31 @@ */ class IndexedService extends Service { - /** - * @var int - */ - public $serviceIndex; + public int $serviceIndex; /** * Note that null and false are NOT the same in this context. - * - * @var bool|null */ - public $isDefault = null; + public ?bool $isDefault = null; - /** - * @param string $location - * @param string $binding - * @param $serviceIndex - * @param bool|null $isDefault - */ - public function __construct($location, $binding, $serviceIndex, $isDefault = null) + public function __construct(string $location, string $binding, int $serviceIndex, ?bool $isDefault = null) { $this->isDefault = $isDefault; $this->serviceIndex = $serviceIndex; parent::__construct($location, $binding); } + + /** + * A convenience static constructor for the IndexedService. + */ + public static function indexedServiceFromArray(array $indexedService): IndexedService + { + return new self( + $indexedService["location"], + $indexedService["binding"], + $indexedService["serviceIndex"], + $indexedService["isDefault"] + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/Logo.php b/src/OpenConext/EngineBlock/Metadata/Logo.php index 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/MetadataRepository/DoctrineMetadataPushRepository.php b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php index 5ee90ecc2a..6c77c40ee8 100644 --- a/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php +++ b/src/OpenConext/EngineBlock/Metadata/MetadataRepository/DoctrineMetadataPushRepository.php @@ -29,31 +29,22 @@ use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use RuntimeException; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class DoctrineMetadataPushRepository { - /** - * @var Connection - */ - private $connection; + private Connection $connection; - /** - * @var ClassMetadata - */ - private $spMetadata; + private ClassMetadata $spMetadata; - /** - * @var ClassMetadata - */ - private $idpMetadata; - - - const ROLES_TABLE_NAME = 'sso_provider_roles_eb5'; + 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 + EntityManager $entityManager, ) { $this->connection = $entityManager->getConnection(); @@ -76,11 +67,11 @@ public function __construct( * @param AbstractRole[] $roles * @return SynchronizationResult * @throws \Exception + * @throws \Throwable */ - public function synchronize(array $roles) + public function synchronize(array $roles): SynchronizationResult { $result = new SynchronizationResult(); - $this->connection->transactional(function () use ($roles, $result): void { $idpsToBeRemoved = $this->findAllRoleEntityIds($this->idpMetadata); $spsToBeRemoved = $this->findAllRoleEntityIds($this->spMetadata); @@ -146,23 +137,21 @@ public function synchronize(array $roles) return $result; } - private function insertRole(AbstractRole $role, ClassMetadata $metadata) + private function insertRole(AbstractRole $role, ClassMetadata $metadata): void { $query = $this->connection->createQueryBuilder() - ->insert(self::ROLES_TABLE_NAME); + ->insert(AbstractRole::TABLE_NAME); $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 $role, ClassMetadata $metadata): void { $query = $this->connection->createQueryBuilder() - ->update(self::ROLES_TABLE_NAME); - + ->update(AbstractRole::TABLE_NAME); $normalized = $this->addUpdateQueryParameters($role, $query, $metadata); $stmt = $this->connection->prepare($query->getSQL()); @@ -170,10 +159,10 @@ 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 { $query = $this->connection->createQueryBuilder() - ->delete(self::ROLES_TABLE_NAME) + ->delete(AbstractRole::TABLE_NAME) ->where('id IN (:ids)') ->setParameter('ids', $roles, ArrayParameterType::INTEGER); @@ -183,11 +172,11 @@ private function deleteRolesByIds(array $roles, ClassMetadata $metadata) return $result; } - private function findAllRoleEntityIds(ClassMetadata $metadata) + private function findAllRoleEntityIds(ClassMetadata $metadata): array|null { $query = $this->connection->createQueryBuilder() ->select('id, entity_id') - ->from(self::ROLES_TABLE_NAME); + ->from(AbstractRole::TABLE_NAME); assert($query instanceof QueryBuilder); $this->addDiscriminatorQuery($query, $metadata); @@ -202,7 +191,7 @@ private function findAllRoleEntityIds(ClassMetadata $metadata) return $results; } - private function addInsertQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata) + private function addInsertQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata): array { $normalized = $this->normalizeData($role, $metadata); foreach (array_keys($normalized) as $id) { @@ -211,7 +200,7 @@ private function addInsertQueryParameters(AbstractRole $role, QueryBuilder $quer return $normalized; } - private function addUpdateQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata) + private function addUpdateQueryParameters(AbstractRole $role, QueryBuilder $query, ClassMetadata $metadata): array { $normalized = $this->normalizeData($role, $metadata); foreach (array_keys($normalized) as $id) { @@ -224,20 +213,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 $role, ClassMetadata $metadata): array { $result = []; foreach ($metadata->fieldMappings as $id => $columnInfo) { diff --git a/src/OpenConext/EngineBlock/Metadata/Organization.php b/src/OpenConext/EngineBlock/Metadata/Organization.php index a9073cdb0c..96c7726449 100644 --- a/src/OpenConext/EngineBlock/Metadata/Organization.php +++ b/src/OpenConext/EngineBlock/Metadata/Organization.php @@ -20,19 +20,26 @@ class Organization { - public $name; - public $displayName; - public $url; + public ?string $name; + public ?string $displayName; + public ?string $url; - /** - * @param $name - * @param $displayName - * @param $url - */ - public function __construct($name, $displayName, $url) + public function __construct(?string $name, ?string $displayName, ?string $url) { - $this->displayName = $displayName; $this->name = $name; + $this->displayName = $displayName; $this->url = $url; } + + /** + * A convenience static constructor for the Organization. + */ + public static function fromArray(array $organization): Organization + { + return new self( + $organization['name'] ?? null, + $organization['displayName'] ?? null, + $organization['url'] ?? null + ); + } } diff --git a/src/OpenConext/EngineBlock/Metadata/RequestedAttribute.php b/src/OpenConext/EngineBlock/Metadata/RequestedAttribute.php index 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/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/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/CertificateArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php new file mode 100644 index 0000000000..f0cd590ef7 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayType.php @@ -0,0 +1,141 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!is_array($value)) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + $value, + $this->getName(), + "null, array" + ) + ); + } + + if (count($value) == 0) { + return null; + } + + $certificates = []; + foreach ($value as $certificate) { + if (!$certificate instanceof X509CertificateLazyProxy) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + $certificate, + $this->getName(), + X509CertificateLazyProxy::class + ) + ); + } + array_push($certificates, $certificate->toCertData()); + } + + return json_encode($certificates); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): array + { + if (is_null($value)) { + return []; + } + + try { + $certificates = []; + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $decoded, + $this->getName(), + "array" + ) + ); + } + + foreach ($decoded as $certificate) { + if (is_string($certificate)) { + array_push($certificates, new X509CertificateLazyProxy(new X509CertificateFactory(), $certificate)); + } else { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $certificate, + $this->getName(), + "X509Certificate" + ) + ); + } + } + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + X509CertificateLazyProxy::class + ), + 0, + $e + ); + } + + return $certificates; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayType.php new file mode 100644 index 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..97a1d3bac1 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationType.php @@ -0,0 +1,116 @@ +getJsonTypeDeclarationSQL($fieldDeclaration); + } + + /** + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (is_null($value)) { + return null; + } + + if (!$value instanceof Organization) { + throw new ConversionException( + sprintf( + 'Value "%s" must be null or an instance of %s (%s) to be able to ' . + 'convert it to a database value', + is_object($value) ? get_class($value) : (string)$value, + $this->getName(), + "null, " . Organization::class + ) + ); + } + + // Drop null properties regardless of what they're called + // This is done as many Organization(s) are potentially null or partially null + $data = array_filter( + get_object_vars($value), + static fn ($field) => $field !== null + ); + + // If every property was null, store NULL instead of "{}" + if ($data === []) { + return null; + } + + return json_encode($data); + } + + /** + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?Organization + { + if (is_null($value)) { + return new Organization(null, null, null); + } + + try { + $decoded = json_decode($value, true); + + if (!is_array($decoded)) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + "array" + ) + ); + } + + $organization = Organization::fromArray($decoded); + } catch (InvalidArgumentException $e) { + throw new ConversionException( + sprintf( + 'Could not convert database value "%s" to Doctrine Type %s. Expected format: %s', + $value, + $this->getName(), + Organization::class + ), + 0, + $e + ); + } + + return $organization; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayType.php b/src/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayType.php new file mode 100644 index 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/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php index caccd0dbc4..53bb9ee4ce 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,11 +163,12 @@ 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); assert($query instanceof Statement); $result = $query->executeQuery(); @@ -200,11 +201,12 @@ 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); assert($query instanceof Statement); $result = $query->executeQuery(); 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/AttributeReleasePolicyTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php new file mode 100644 index 0000000000..c9548d46d3 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/AttributeReleasePolicyTypeTest.php @@ -0,0 +1,137 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $value = $arpType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function attribute_release_policy_converted_to_json() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + $arp = array(); + $arp["uid"] = ["value" => "*", "motiviation" => ""]; + $arp["givenName"] = ["value" => "*", "motiviation" => ""]; + $arp["attribute"] = ["value" => "*", "motiviation" => ""]; + $attributeReleasePolicy = new AttributeReleasePolicy($arp); + + $value = $arpType->convertToDatabaseValue($attributeReleasePolicy, $this->platform); + + $this->assertEquals(json_encode($attributeReleasePolicy->getAttributeRules()), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_null() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $value = $arpType->convertToPHPValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $arp = array(); + $arp["uid"] = ["value" => "*", "motiviation" => ""]; + $arp["givenName"] = ["value" => "*", "motiviation" => ""]; + $arp["attribute"] = ["value" => "*", "motiviation" => ""]; + $attributeReleasePolicy = new AttributeReleasePolicy($arp); + + $value = $arpType->convertToPHPValue($arpType->convertToDatabaseValue($attributeReleasePolicy, $this->platform), + $this->platform); + + $this->assertEquals($attributeReleasePolicy, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $this->expectException(ConversionException::class); + $arpType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $arpType = Type::getType(AttributeReleasePolicyType::NAME); + + $this->expectException(ConversionException::class); + $arpType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php new file mode 100644 index 0000000000..79e858553d --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/CertificateArrayTypeTest.php @@ -0,0 +1,144 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_or_empty_array_is_null_conversion() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $value = $certType->convertToDatabaseValue(null, $this->platform); + $emptyArray = $certType->convertToDatabaseValue([], $this->platform); + + $this->assertNull($value); + $this->assertNull($emptyArray); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function certificate_array_type_converted_to_json() + { + $certType = Type::getType(CertificateArrayType::NAME); + $certificateArray = [new X509CertificateLazyProxy(new X509CertificateFactory(), $this->certData)]; + + $value = $certType->convertToDatabaseValue($certificateArray, $this->platform); + + $this->assertEquals(json_encode([$certificateArray[0]->toCertData()]), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_empty_array() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $value = $certType->convertToPHPValue(null, $this->platform); + + $this->assertEquals([], $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_equals_result() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $certificateArray = [new X509CertificateLazyProxy(new X509CertificateFactory(), $this->certData)]; + + $value = $certType->convertToPHPValue($certType->convertToDatabaseValue($certificateArray, $this->platform), + $this->platform); + + $this->assertEquals($certificateArray[0]->toCertData(), $value[0]->toCertData()); + + // Verify that the certificate is actually valid. + $factory = new X509CertificateFactory(); + // baseline + $this->assertNotFalse(openssl_x509_read($factory->fromCertData($this->certData)->toPem())); + // converted + $this->assertNotFalse(openssl_x509_read($factory->fromCertData($value[0]->toCertData())->toPem())); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $this->expectException(ConversionException::class); + $this->expectExceptionMessage('Value "" must be null or an instance of engineblock_certificate_array (null, array) to be able to convert it to a database value'); + + $certType->convertToDatabaseValue("", $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $certType = Type::getType(CertificateArrayType::NAME); + + $this->expectException(ConversionException::class); + $this->expectExceptionMessage('Could not convert database value "" to Doctrine Type engineblock_certificate_array. Expected format: X509Certificate'); + + $certType->convertToPHPValue("[false]", $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/ContactPersonArrayTypeTest.php new file mode 100644 index 0000000000..270ad309dc --- /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..40dedb683a --- /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..be79b26a11 --- /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..5b0f14770b --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/OrganizationTypeTest.php @@ -0,0 +1,176 @@ +platform = new MySQLPlatform(); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_remains_null_in_to_sql_conversion() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $value = $organizationType->convertToDatabaseValue(null, $this->platform); + + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_type_with_all_fields_converted_to_json() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", "displayName", "url"); + $value = $organizationType->convertToDatabaseValue($organization, $this->platform); + + $this->assertEquals(json_encode($organization), $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_with_null_values_filters_them_out() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", null, "url"); + $value = $organizationType->convertToDatabaseValue($organization, $this->platform); + + // Should not contain the null displayName field + $decoded = json_decode($value, true); + $this->assertArrayNotHasKey('displayName', $decoded); + $this->assertArrayHasKey('name', $decoded); + $this->assertArrayHasKey('url', $decoded); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_with_all_null_values_returns_null() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization(null, null, null); + $value = $organizationType->convertToDatabaseValue($organization, $this->platform); + + // All null values should result in NULL being stored + $this->assertNull($value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function a_null_value_is_converted_to_organization_with_all_nulls() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $value = $organizationType->convertToPHPValue(null, $this->platform); + + $this->assertInstanceOf(Organization::class, $value); + $this->assertNull($value->name); + $this->assertNull($value->displayName); + $this->assertNull($value->url); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function organization_with_partial_null_values_roundtrips_correctly() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", null, "url"); + + $databaseValue = $organizationType->convertToDatabaseValue($organization, $this->platform); + $value = $organizationType->convertToPHPValue($databaseValue, $this->platform); + + // After roundtrip, should have the non-null values + $this->assertEquals("name", $value->name); + $this->assertNull($value->displayName); + $this->assertEquals("url", $value->url); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function saved_object_with_all_fields_equals_result() + { + $organizationType = Type::getType(OrganizationType::NAME); + $organization = new Organization("name", "displayName", "url"); + + $value = $organizationType->convertToPHPValue($organizationType->convertToDatabaseValue($organization, $this->platform), + $this->platform); + + $this->assertEquals($organization, $value); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_php_value_causes_an_exception_upon_conversion() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $this->expectException(ConversionException::class); + $organizationType->convertToDatabaseValue(false, $this->platform); + } + + #[Group('EngineBlockBundle')] + #[Group('Doctrine')] + #[Test] + public function an_invalid_database_value_causes_an_exception_upon_conversion() + { + $organizationType = Type::getType(OrganizationType::NAME); + + $this->expectException(ConversionException::class); + $organizationType->convertToPHPValue(false, $this->platform); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php b/tests/unit/OpenConext/EngineBlockBundle/Doctrine/Type/RequestedAttributeArrayTypeTest.php new file mode 100644 index 0000000000..571130ae1f --- /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..b66cb93089 --- /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..25e9e121b7 --- /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..46009422e9 --- /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); + } +}