From c2c44b9a39918009873330dced9655ac6317a29b Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 5 May 2023 10:39:31 -0500 Subject: [PATCH] Upgrade symfony to 5.4 & separate EntityMapper (#174) * Update to Symfony 5.4 * Switch to lexik_jwt_bundle * Milliner needs islandora-fedora-entity-mapper but not Crayfish-Commons * Fix tests * Update dependencies to tagged versions * Remove unused imports * Github actions workflow * Update defaults to include the `user_identify_field` configuration parameter * Add upgrade docs * Update nodejs actions for workflows --- .github/workflows/build-3.x.yml | 19 ++- .github/workflows/build-4.x.yml | 81 +++++++++++ Homarus/.env | 9 +- Homarus/README.md | 10 +- Homarus/UPGRADE.md | 37 +++++ Homarus/composer.json | 25 ++-- Homarus/config/bootstrap.php | 23 --- Homarus/config/bundles.php | 1 + Homarus/config/packages/crayfish_commons.yaml | 2 +- Homarus/config/packages/framework.yaml | 11 +- .../packages/lexik_jwt_authentication.yaml | 9 ++ Homarus/config/packages/security.yaml | 27 ++-- Homarus/config/preload.php | 4 - Homarus/public/index.php | 26 +--- Homarus/src/Kernel.php | 43 ------ Homarus/symfony.lock | 31 +++- Homarus/tests/HomarusControllerTest.php | 8 +- Houdini/.env | 9 +- Houdini/.gitignore | 4 + Houdini/README.md | 16 ++- Houdini/UPGRADE.md | 33 +++++ Houdini/composer.json | 25 ++-- Houdini/config/bootstrap.php | 23 --- Houdini/config/bundles.php | 1 + Houdini/config/packages/crayfish_commons.yaml | 1 - Houdini/config/packages/framework.yaml | 12 +- .../packages/lexik_jwt_authentication.yaml | 9 ++ Houdini/config/packages/security.yaml | 28 ++-- Houdini/config/preload.php | 2 +- Houdini/public/index.php | 26 +--- Houdini/src/Kernel.php | 43 ------ Houdini/symfony.lock | 31 +++- Houdini/tests/HoudiniControllerTest.php | 7 +- Hypercube/.env | 9 +- Hypercube/.gitignore | 4 + Hypercube/README.md | 10 +- Hypercube/UPGRADE.md | 33 +++++ Hypercube/composer.json | 23 +-- Hypercube/config/bootstrap.php | 23 --- Hypercube/config/bundles.php | 1 + .../config/packages/crayfish_commons.yaml | 1 - Hypercube/config/packages/framework.yaml | 11 +- .../packages/lexik_jwt_authentication.yaml | 9 ++ Hypercube/config/packages/security.yaml | 23 ++- Hypercube/config/preload.php | 4 - Hypercube/public/index.php | 26 +--- .../src/Controller/HypercubeController.php | 12 +- Hypercube/src/Kernel.php | 43 ------ Hypercube/symfony.lock | 31 +++- Hypercube/tests/HypercubeControllerTest.php | 5 + Milliner/.env | 9 +- Milliner/README.md | 13 +- Milliner/UPGRADE.md | 34 +++++ Milliner/composer.json | 26 ++-- Milliner/config/bootstrap.php | 23 --- Milliner/config/bundles.php | 2 +- .../config/packages/crayfish_commons.yaml | 4 - Milliner/config/packages/framework.yaml | 11 +- .../packages/lexik_jwt_authentication.yaml | 9 ++ Milliner/config/packages/security.yaml | 19 ++- Milliner/config/preload.php | 4 - Milliner/config/services.yaml | 1 + Milliner/public/index.php | 26 +--- .../src/Controller/MillinerController.php | 33 +++-- Milliner/src/Kernel.php | 43 ------ Milliner/src/Service/MillinerService.php | 73 +++++----- .../src/Service/MillinerServiceInterface.php | 12 +- Milliner/symfony.lock | 32 +++-- Milliner/tests/AbstractMillinerTestCase.php | 136 ++++++++++++------ Milliner/tests/CreateVersionTest.php | 40 ++++-- Milliner/tests/DeleteTest.php | 37 +++-- Milliner/tests/SaveExternalTest.php | 7 +- Milliner/tests/SaveMediaTest.php | 67 ++++----- Milliner/tests/SaveNodeTest.php | 65 +++++---- Recast/.env | 9 +- Recast/README.md | 7 +- Recast/UPGRADE.md | 33 +++++ Recast/composer.json | 24 ++-- Recast/config/bootstrap.php | 23 --- Recast/config/bundles.php | 1 + Recast/config/packages/crayfish_commons.yaml | 1 - Recast/config/packages/framework.yaml | 11 +- .../packages/lexik_jwt_authentication.yaml | 9 ++ Recast/config/packages/security.yaml | 19 ++- Recast/config/preload.php | 5 +- Recast/public/index.php | 26 +--- Recast/src/Controller/RecastController.php | 26 ++-- Recast/src/Kernel.php | 43 ------ Recast/symfony.lock | 31 +++- Recast/tests/RecastControllerTest.php | 28 ++-- 90 files changed, 1052 insertions(+), 874 deletions(-) create mode 100644 .github/workflows/build-4.x.yml delete mode 100644 Homarus/config/bootstrap.php create mode 100644 Homarus/config/packages/lexik_jwt_authentication.yaml create mode 100644 Houdini/UPGRADE.md delete mode 100644 Houdini/config/bootstrap.php create mode 100644 Houdini/config/packages/lexik_jwt_authentication.yaml delete mode 100644 Hypercube/config/bootstrap.php create mode 100644 Hypercube/config/packages/lexik_jwt_authentication.yaml delete mode 100644 Milliner/config/bootstrap.php delete mode 100644 Milliner/config/packages/crayfish_commons.yaml create mode 100644 Milliner/config/packages/lexik_jwt_authentication.yaml delete mode 100644 Recast/config/bootstrap.php create mode 100644 Recast/config/packages/lexik_jwt_authentication.yaml diff --git a/.github/workflows/build-3.x.yml b/.github/workflows/build-3.x.yml index ae1285aa..267c661b 100644 --- a/.github/workflows/build-3.x.yml +++ b/.github/workflows/build-3.x.yml @@ -29,12 +29,12 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: build_dir - name: Checkout islandora_ci - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: islandora/islandora_ci ref: github-actions @@ -50,11 +50,18 @@ jobs: run: | echo "SCRIPT_DIR=$GITHUB_WORKSPACE/islandora_ci" >> $GITHUB_ENV + - name: Get composer cache directory + id: composer-cache + run: | + cd $GITHUB_WORKSPACE/build_dir/Milliner + echo "composer-cache-dir=$(composer config cache-files-dir)" >> $GITHUB_ENV + - name: Cache Composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: - path: /tmp/composer-cache - key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + path: ${{ env.composer-cache-dir }} + key: ${{ runner.os }}-composer-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer-${{ matrix.php-version }} - name: composer install run: | @@ -70,5 +77,5 @@ jobs: .scripts/tester - name: codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 diff --git a/.github/workflows/build-4.x.yml b/.github/workflows/build-4.x.yml new file mode 100644 index 00000000..dd912476 --- /dev/null +++ b/.github/workflows/build-4.x.yml @@ -0,0 +1,81 @@ +# This is a basic workflow to help you get started with Actions + +name: CI - 4.x + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the 7.x branch + push: + branches: [ 4.x ] + pull_request: + branches: [ 4.x ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ["7.4", "8.0", "8.1"] + + name: PHP ${{ matrix.php-versions }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout code + uses: actions/checkout@v3 + with: + path: build_dir + + - name: Checkout islandora_ci + uses: actions/checkout@v3 + with: + repository: islandora/islandora_ci + ref: github-actions + path: islandora_ci + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: composer:v2 + + - name: Set environment variables + run: | + echo "SCRIPT_DIR=$GITHUB_WORKSPACE/islandora_ci" >> $GITHUB_ENV + + - name: Get composer cache directory + id: composer-cache + run: | + cd $GITHUB_WORKSPACE/build_dir/Milliner + echo "composer-cache-dir=$(composer config cache-files-dir)" >> $GITHUB_ENV + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: ${{ env.composer-cache-dir }} + key: ${{ runner.os }}-composer-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer-${{ matrix.php-version }} + + - name: composer install + run: | + cd $GITHUB_WORKSPACE/build_dir + for D in */; do (cd $D; composer install) done + + - name: line endings + run: $SCRIPT_DIR/line_endings.sh $GITHUB_WORKSPACE + + - name: test scripts + run: | + cd $GITHUB_WORKSPACE/build_dir + .scripts/tester + + - name: codecov + uses: codecov/codecov-action@v3 + diff --git a/Homarus/.env b/Homarus/.env index 3a9196ee..a6f7b9c0 100644 --- a/Homarus/.env +++ b/Homarus/.env @@ -9,9 +9,10 @@ # Real environment variables win over .env files. # # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/5.4/configuration/secrets.html # # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration +# https://symfony.com/doc/5.4/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### APP_ENV=dev @@ -19,3 +20,9 @@ APP_SECRET=2debbf0f3bc4a9484b577b8952dc3477 #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=900bcb1e6df36b802204915dbe1c2f6a +###< lexik/jwt-authentication-bundle ### diff --git a/Homarus/README.md b/Homarus/README.md index cd2ff632..7c17362a 100644 --- a/Homarus/README.md +++ b/Homarus/README.md @@ -49,9 +49,6 @@ the `app.executable` parameter in [`/path/to/Homarus/config/services.yaml`](./co You also need to set your Fedora Base Url to allow the Fedora Resource to be pulled in automatically. This is done in the `/path/to/Homarus/config/packages/crayfish_commons.yaml`. -Also in the `/path/to/Homarus/config/packages/crayfish_commons.yaml` file you can point to the location of your `syn-settings.xml`. -If you don't have a `syn-settings.xml` look at the [Syn](http://github.com/Islandora/Syn) documentation. - ### Logging To change your log settings, edit the `/path/to/Homarus/config/packages/monolog.yaml` file. @@ -62,10 +59,13 @@ environment directory will take precedence over those in the `/path/to/Homarus/c The location specified in the configuration file for the log must be writable by the web server. -### Disabling Syn +### Enabling JWT authentication There are instructions in the `/path/to/Homarus/config/packages/security.yaml` file describing what to change and what lines -to comment out to disable Syn. +to comment out to enable authentication. + +We use the Lexik JWT Authentication Bundle for Symfony, more information here +https://github.com/lexik/LexikJWTAuthenticationBundle ## Usage This will return the an AVI file for the test video file in Fedora. diff --git a/Homarus/UPGRADE.md b/Homarus/UPGRADE.md index 05ac5be0..42a5561b 100644 --- a/Homarus/UPGRADE.md +++ b/Homarus/UPGRADE.md @@ -1,5 +1,42 @@ This document guides you through the process of upgrading Homarus. First, check if a section named "Upgrade to x.x.x" exists, with x.x.x being the version you are planning to upgrade to. +## Upgrade to 4.0.0 + +4.0.0 uses Symfony 5.4, which has differences from 3.x.x which used Symfony 4.4. +This makes it non-backwards compatible and requires testing of any custom changes you +may have made. + +Homarus relies on Crayfish-Commons `^4.0` which no longer includes our own JWT authentication, +to perform JWT authentication we use the [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle). + +### Upgrade from version 3.x.x + +You can remove the `syn_config:` line from the `./config/packages/crayfish_commons.yaml` file. + +You can add a line like `apix_middleware_enabled: false` to the `./config/packages/crayfish_commons.yaml` file. +This disables the ApixMiddleware as we pass the full URL to ffmpeg instead of downloading the file and passing +it directly. + +You will need to make a file in `./config/packages` called `lexik_jwt_authentication.yaml`. + +The file needs to contain (at a minimum) or you can copy the file from the Github repository: +```yaml +lexik_jwt_authentication: + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub +``` + +You can either: +* define an [environment variables](https://symfony.com/doc/5.4/configuration.html#configuration-based-on-environment-variables) for + the `JWT_PUBLIC_KEY` variable defined above and pointed to the Drupal public key file + +_or_ +* explicitly overwrite the `'%env(resolve:JWT_PUBLIC_KEY)%'` in + the above file and specify the path to the Drupal public key + + ## Upgrade to 3.0.0 Homarus (and all of Crayfish) adheres to [semantic versioning](https://semver.org), which makes a distinction between "major", "minor", and "patch" versions. The upgrade path will be different depending on which previous version from which you are migrating. diff --git a/Homarus/composer.json b/Homarus/composer.json index c3fff258..4680307c 100644 --- a/Homarus/composer.json +++ b/Homarus/composer.json @@ -6,22 +6,26 @@ "require": { "ext-ctype": "*", "ext-iconv": "*", - "islandora/crayfish-commons": "^3.0", - "symfony/dotenv": "4.4.*", + "islandora/crayfish-commons": "^4.0", + "lexik/jwt-authentication-bundle": "^2.18", + "symfony/dotenv": "5.4.*", "symfony/flex": "^1.3.1", - "symfony/framework-bundle": "4.4.*", - "symfony/yaml": "4.4.*" + "symfony/framework-bundle": "5.4.*", + "symfony/runtime": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/yaml": "5.4.*" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "sebastian/phpcpd": "^6.0", "squizlabs/php_codesniffer": "^3.0", - "symfony/var-dumper": "4.4.*", - "symfony/browser-kit": "4.4.*", - "symfony/css-selector": "4.4.*", + "symfony/var-dumper": "5.4.*", + "symfony/browser-kit": "5.4.*", + "symfony/css-selector": "5.4.*", "symfony/maker-bundle": "^1.0", - "symfony/phpunit-bridge": "4.4.*" + "symfony/phpunit-bridge": "5.4.*" }, "minimum-stability": "dev", "prefer-stable": true, @@ -31,7 +35,8 @@ }, "sort-packages": true, "allow-plugins": { - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": true } }, "autoload": { @@ -68,7 +73,7 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "4.4.*" + "require": "5.4.*" } }, "authors": [ diff --git a/Homarus/config/bootstrap.php b/Homarus/config/bootstrap.php deleted file mode 100644 index 55560fb8..00000000 --- a/Homarus/config/bootstrap.php +++ /dev/null @@ -1,23 +0,0 @@ -=1.2) -if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { - (new Dotenv(false))->populate($env); -} else { - // load all the .env files - (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); -} - -$_SERVER += $_ENV; -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/Homarus/config/bundles.php b/Homarus/config/bundles.php index 127ed6c5..ac52d15f 100644 --- a/Homarus/config/bundles.php +++ b/Homarus/config/bundles.php @@ -5,4 +5,5 @@ Islandora\Crayfish\Commons\CrayfishCommonsBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; diff --git a/Homarus/config/packages/crayfish_commons.yaml b/Homarus/config/packages/crayfish_commons.yaml index 92c45c3e..1b72cdbf 100644 --- a/Homarus/config/packages/crayfish_commons.yaml +++ b/Homarus/config/packages/crayfish_commons.yaml @@ -1,3 +1,3 @@ crayfish_commons: fedora_base_uri: 'http://localhost:8080/fcrepo/rest' - #syn_config: '/path/to/syn-settings.xml' + apix_middleware_enabled: false diff --git a/Homarus/config/packages/framework.yaml b/Homarus/config/packages/framework.yaml index cad7f780..a375c3e7 100644 --- a/Homarus/config/packages/framework.yaml +++ b/Homarus/config/packages/framework.yaml @@ -1,8 +1,8 @@ -# see https://symfony.com/doc/current/reference/configuration/framework.html +# see https://symfony.com/doc/5.4/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' #csrf_protection: true - #http_method_override: true + http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. @@ -10,8 +10,15 @@ framework: handler_id: null cookie_secure: auto cookie_samesite: lax + storage_factory_id: session.storage.factory.native #esi: true #fragments: true php_errors: log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/Homarus/config/packages/lexik_jwt_authentication.yaml b/Homarus/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 00000000..c7a868e8 --- /dev/null +++ b/Homarus/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,9 @@ +lexik_jwt_authentication: + # Need secret key to generate a token, this is not necessary for normal usage as the key is generated by Drupal. + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + # This is required if you have set a passphrase on the secret key, this is generally not needed. + pass_phrase: '%env(resolve:JWT_PASSPHRASE)%' + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub diff --git a/Homarus/config/packages/security.yaml b/Homarus/config/packages/security.yaml index edc5b619..7dc90667 100644 --- a/Homarus/config/packages/security.yaml +++ b/Homarus/config/packages/security.yaml @@ -1,10 +1,10 @@ security: - - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + enable_authenticator_manager: true + # https://symfony.com/doc/5.4/security.html#where-do-users-come-from-user-providers providers: - jwt_user_provider: - id: Islandora\Crayfish\Commons\Syn\JwtUserProvider - + users_in_memory: { memory: null } + jwt: + lexik_jwt: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -15,21 +15,18 @@ security: # Need stateless or it reloads the User based on a token. stateless: true - # To enable Syn, uncomment the below 4 lines and change anonymous to false above. - #provider: jwt_user_provider - #guard: - # authenticators: - # - Islandora\Crayfish\Commons\Syn\JwtAuthenticator + # To enable JWT authentication, uncomment the below 2 lines and change anonymous to false above. + #provider: jwt + #jwt: ~ # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#firewalls-authentication + # https://symfony.com/doc/5.4/security.html#firewalls-authentication - # https://symfony.com/doc/current/security/impersonating_user.html + # https://symfony.com/doc/5.4/security/impersonating_user.html # switch_user: true - # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } diff --git a/Homarus/config/preload.php b/Homarus/config/preload.php index 064bdcd6..5ebcdb21 100644 --- a/Homarus/config/preload.php +++ b/Homarus/config/preload.php @@ -1,9 +1,5 @@ handle($request); -$response->send(); -$kernel->terminate($request, $response); +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/Homarus/src/Kernel.php b/Homarus/src/Kernel.php index 2b9a2f8d..f2eca215 100644 --- a/Homarus/src/Kernel.php +++ b/Homarus/src/Kernel.php @@ -3,52 +3,9 @@ namespace App\Islandora\Homarus; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Component\Routing\RouteCollectionBuilder; class Kernel extends BaseKernel { use MicroKernelTrait; - - private const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - public function registerBundles(): iterable - { - $contents = require $this->getProjectDir().'/config/bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - - public function getProjectDir(): string - { - return \dirname(__DIR__); - } - - protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void - { - $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); - $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); - } - - protected function configureRoutes(RouteCollectionBuilder $routes): void - { - $confDir = $this->getProjectDir().'/config'; - - $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); - } } diff --git a/Homarus/symfony.lock b/Homarus/symfony.lock index 2df3f809..4837d54d 100644 --- a/Homarus/symfony.lock +++ b/Homarus/symfony.lock @@ -23,6 +23,18 @@ "islandora/crayfish-commons": { "version": "3.0.0" }, + "lexik/jwt-authentication-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, "ml/iri": { "version": "1.1.4" }, @@ -228,12 +240,12 @@ "version": "v4.4.30" }, "symfony/flex": { - "version": "1.17", + "version": "1.19", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", + "branch": "main", "version": "1.0", - "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" }, "files": [ ".env" @@ -382,6 +394,19 @@ "symfony/service-contracts": { "version": "v2.4.0" }, + "symfony/translation": { + "version": "5.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/var-dumper": { "version": "v4.4.31" }, diff --git a/Homarus/tests/HomarusControllerTest.php b/Homarus/tests/HomarusControllerTest.php index 8d8fbd3e..8e5727d8 100644 --- a/Homarus/tests/HomarusControllerTest.php +++ b/Homarus/tests/HomarusControllerTest.php @@ -21,9 +21,9 @@ class HomarusControllerTest extends TestCase use ProphecyTrait; - private $defaults; + private array $defaults; - private $formats; + private array $formats; /** * Setup to reset to defaults. @@ -211,6 +211,10 @@ private function getDefaultController() { // Mock a CmdExecuteService. $prophecy = $this->prophesize(CmdExecuteService::class); + $prophecy->execute(Argument::any(), Argument::any()) + ->willReturn(function () { + return null; + }); $mock_service = $prophecy->reveal(); // Create a controller. diff --git a/Houdini/.env b/Houdini/.env index 50aa74bd..a036558f 100644 --- a/Houdini/.env +++ b/Houdini/.env @@ -9,9 +9,10 @@ # Real environment variables win over .env files. # # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/5.4/configuration/secrets.html # # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration +# https://symfony.com/doc/5.4/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### APP_ENV=dev @@ -19,3 +20,9 @@ APP_SECRET=6dc39d9beae73673bd864765298482be #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 #TRUSTED_HOSTS='^localhost|example\.com$' ###< symfony/framework-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=96f0a4e61ad546e6a384119196ae330a +###< lexik/jwt-authentication-bundle ### diff --git a/Houdini/.gitignore b/Houdini/.gitignore index d49a79c9..de800f4b 100644 --- a/Houdini/.gitignore +++ b/Houdini/.gitignore @@ -9,3 +9,7 @@ /phpunit.xml .phpunit.result.cache ###< phpunit/phpunit ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### diff --git a/Houdini/README.md b/Houdini/README.md index 66399d01..b5b0e901 100644 --- a/Houdini/README.md +++ b/Houdini/README.md @@ -12,6 +12,10 @@ - `$ cd /path/to/Houdini` and run `$ composer install` - For production, configure your web server appropriately (e.g. add a VirtualHost for Houdini in Apache) +## Upgrading + +Steps for upgrading Houdini can be found in [UPGRADE.md](UPGRADE.md) + ## Configuration Symfony uses `.dotenv` to set environment variables. You can check the [.env](./.env) in the root of the Houdini directory. @@ -24,8 +28,7 @@ If your `imagemagick` installation is not on your path, then you can configure H the `app.executable` parameter in [`/path/to/Houdini/config/services.yaml`](./config/services.yaml). You also need to set your Fedora Base Url to allow the Fedora Resource to be pulled in automatically. This is done in the -`/path/to/Houdini/config/packages/crayfish_commons.yaml`. In the same file you can point to the location of your `syn-settings.xml`. -If you don't have a `syn-settings.xml` look at the [Syn](http://github.com/Islandora/Syn) documentation. +`/path/to/Houdini/config/packages/crayfish_commons.yaml`. ### Logging @@ -37,10 +40,13 @@ environment directory will take precedence over those in the `/path/to/Houdini/c The location specified in the configuration file for the log must be writable by the web server. -### Disabling Syn +### Enabling JWT authentication There are instructions in the `/path/to/Houdini/config/packages/security.yaml` file describing what to change and what lines -to comment out to disable Syn. +to comment out to enable authentication. + +We use the Lexik JWT Authentication Bundle for Symfony, more information here +https://github.com/lexik/LexikJWTAuthenticationBundle ## Usage @@ -49,7 +55,7 @@ Houdini sets up two endpoints: - /convert/ Houdini is meant for use with API-X, and accepts `GET` and `OPTIONS` requests to those endpoints. The `OPTIONS` requests are for use with the API-X service loading mechanism, and return RDF describing the -service for API-X. The `GET` requests are used to execute the services, and must contain the URI to an image in Fedora in the `ApixLdpResource` header. +service for API-X. The `GET` requests are used to execute the services, and must contain the URI to an image in Fedora in the `Apix-Ldp-Resource` header. ### Identify diff --git a/Houdini/UPGRADE.md b/Houdini/UPGRADE.md new file mode 100644 index 00000000..c6c7dcf4 --- /dev/null +++ b/Houdini/UPGRADE.md @@ -0,0 +1,33 @@ +This document guides you through the process of upgrading Houdini. First, check if a section named "Upgrade to x.x.x" exists, with x.x.x being the version you are planning to upgrade to. + +## Upgrade to 4.0.0 + +4.0.0 uses Symfony 5.4, which has differences from 3.x.x which used Symfony 4.4. +This makes it non-backwards compatible and requires testing of any custom changes you +may have made. + +Houdini relies on Crayfish-Commons `^4.0` which no longer includes our own JWT authentication, +to perform JWT authentication we use the [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle). + +### Upgrade from version 3.x.x + +You can remove the `syn_config:` line from the `./config/packages/crayfish_commons.yaml` file. + +You will need to make a file in `./config/packages` called `lexik_jwt_authentication.yaml`. + +The file needs to contain (at a minimum) or you can copy the file from the Github repository: +```yaml +lexik_jwt_authentication: + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub +``` + +You can either: +* define an [environment variables](https://symfony.com/doc/5.4/configuration.html#configuration-based-on-environment-variables) for + the `JWT_PUBLIC_KEY` variable defined above and pointed to the Drupal public key file + +_or_ +* explicitly overwrite the `'%env(resolve:JWT_PUBLIC_KEY)%'` in + the above file and specify the path to the Drupal public key diff --git a/Houdini/composer.json b/Houdini/composer.json index 03b8ff34..3d84612f 100644 --- a/Houdini/composer.json +++ b/Houdini/composer.json @@ -18,22 +18,26 @@ "require": { "ext-ctype": "*", "ext-iconv": "*", - "islandora/crayfish-commons": "^3.0", - "symfony/dotenv": "4.4.*", + "islandora/crayfish-commons": "^4.0", + "lexik/jwt-authentication-bundle": "^2.18", + "symfony/dotenv": "5.4.*", "symfony/flex": "^1.17", - "symfony/framework-bundle": "4.4.*", + "symfony/framework-bundle": "5.4.*", "symfony/monolog-bundle": "^3.4", - "symfony/yaml": "4.4.*" + "symfony/runtime": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/yaml": "5.4.*" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "sebastian/phpcpd": "^6.0", "squizlabs/php_codesniffer": "^3.0", - "symfony/browser-kit": "4.4.*", - "symfony/css-selector": "4.4.*", - "symfony/phpunit-bridge": "4.4.*", - "symfony/var-dumper": "4.4.*" + "symfony/browser-kit": "5.4.*", + "symfony/css-selector": "5.4.*", + "symfony/phpunit-bridge": "5.4.*", + "symfony/var-dumper": "5.4.*" }, "minimum-stability": "dev", "prefer-stable": true, @@ -43,7 +47,8 @@ }, "sort-packages": true, "allow-plugins": { - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": true } }, "autoload": { @@ -80,7 +85,7 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "4.4.*" + "require": "5.4.*" } } } diff --git a/Houdini/config/bootstrap.php b/Houdini/config/bootstrap.php deleted file mode 100644 index 0efa55f5..00000000 --- a/Houdini/config/bootstrap.php +++ /dev/null @@ -1,23 +0,0 @@ -=1.2) -if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { - (new Dotenv(false))->populate($env); -} else { - // load all the .env files - (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); -} - -$_SERVER += $_ENV; -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/Houdini/config/bundles.php b/Houdini/config/bundles.php index eb075703..41002225 100644 --- a/Houdini/config/bundles.php +++ b/Houdini/config/bundles.php @@ -5,4 +5,5 @@ Islandora\Crayfish\Commons\CrayfishCommonsBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; diff --git a/Houdini/config/packages/crayfish_commons.yaml b/Houdini/config/packages/crayfish_commons.yaml index 92c45c3e..6ebda51b 100644 --- a/Houdini/config/packages/crayfish_commons.yaml +++ b/Houdini/config/packages/crayfish_commons.yaml @@ -1,3 +1,2 @@ crayfish_commons: fedora_base_uri: 'http://localhost:8080/fcrepo/rest' - #syn_config: '/path/to/syn-settings.xml' diff --git a/Houdini/config/packages/framework.yaml b/Houdini/config/packages/framework.yaml index 5e25aa4b..a375c3e7 100644 --- a/Houdini/config/packages/framework.yaml +++ b/Houdini/config/packages/framework.yaml @@ -1,16 +1,24 @@ +# see https://symfony.com/doc/5.4/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' #csrf_protection: true - #http_method_override: true + http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. session: - handler_id: ~ + handler_id: null cookie_secure: auto cookie_samesite: lax + storage_factory_id: session.storage.factory.native #esi: true #fragments: true php_errors: log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/Houdini/config/packages/lexik_jwt_authentication.yaml b/Houdini/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 00000000..c7a868e8 --- /dev/null +++ b/Houdini/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,9 @@ +lexik_jwt_authentication: + # Need secret key to generate a token, this is not necessary for normal usage as the key is generated by Drupal. + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + # This is required if you have set a passphrase on the secret key, this is generally not needed. + pass_phrase: '%env(resolve:JWT_PASSPHRASE)%' + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub diff --git a/Houdini/config/packages/security.yaml b/Houdini/config/packages/security.yaml index 5195687a..7dc90667 100644 --- a/Houdini/config/packages/security.yaml +++ b/Houdini/config/packages/security.yaml @@ -1,11 +1,10 @@ -# To disable Syn checking, set syn_enabled=false in crayfish_commons.yaml and remove this configuration file. security: - - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + enable_authenticator_manager: true + # https://symfony.com/doc/5.4/security.html#where-do-users-come-from-user-providers providers: - jwt_user_provider: - id: Islandora\Crayfish\Commons\Syn\JwtUserProvider - + users_in_memory: { memory: null } + jwt: + lexik_jwt: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -16,21 +15,18 @@ security: # Need stateless or it reloads the User based on a token. stateless: true - # To enable Syn, uncomment the below 4 lines and change anonymous to false above. - #provider: jwt_user_provider - #guard: - # authenticators: - # - Islandora\Crayfish\Commons\Syn\JwtAuthenticator + # To enable JWT authentication, uncomment the below 2 lines and change anonymous to false above. + #provider: jwt + #jwt: ~ # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#firewalls-authentication + # https://symfony.com/doc/5.4/security.html#firewalls-authentication - # https://symfony.com/doc/current/security/impersonating_user.html + # https://symfony.com/doc/5.4/security/impersonating_user.html # switch_user: true - # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } diff --git a/Houdini/config/preload.php b/Houdini/config/preload.php index b930de96..5ebcdb21 100644 --- a/Houdini/config/preload.php +++ b/Houdini/config/preload.php @@ -2,4 +2,4 @@ if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) { require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php'; -} \ No newline at end of file +} diff --git a/Houdini/public/index.php b/Houdini/public/index.php index e350da16..a6a7aaa0 100644 --- a/Houdini/public/index.php +++ b/Houdini/public/index.php @@ -1,27 +1,9 @@ handle($request); -$response->send(); -$kernel->terminate($request, $response); +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/Houdini/src/Kernel.php b/Houdini/src/Kernel.php index 065031d4..fa0736e6 100644 --- a/Houdini/src/Kernel.php +++ b/Houdini/src/Kernel.php @@ -3,52 +3,9 @@ namespace App\Islandora\Houdini; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Component\Routing\RouteCollectionBuilder; class Kernel extends BaseKernel { use MicroKernelTrait; - - private const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - public function registerBundles(): iterable - { - $contents = require $this->getProjectDir().'/config/bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - - public function getProjectDir(): string - { - return \dirname(__DIR__); - } - - protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void - { - $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); - $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); - } - - protected function configureRoutes(RouteCollectionBuilder $routes): void - { - $confDir = $this->getProjectDir().'/config'; - - $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); - } } diff --git a/Houdini/symfony.lock b/Houdini/symfony.lock index c2d97093..3e30e21e 100644 --- a/Houdini/symfony.lock +++ b/Houdini/symfony.lock @@ -20,6 +20,18 @@ "islandora/crayfish-commons": { "version": "3.0.0" }, + "lexik/jwt-authentication-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, "ml/iri": { "version": "1.1.4" }, @@ -281,18 +293,12 @@ "tests/.gitignore" ] }, - "symfony/polyfill-intl-idn": { - "version": "v1.11.0" - }, "symfony/polyfill-intl-normalizer": { "version": "v1.20.0" }, "symfony/polyfill-mbstring": { "version": "v1.11.0" }, - "symfony/polyfill-php72": { - "version": "v1.11.0" - }, "symfony/polyfill-php73": { "version": "v1.11.0" }, @@ -347,6 +353,19 @@ "symfony/service-contracts": { "version": "v1.1.5" }, + "symfony/translation": { + "version": "5.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/var-dumper": { "version": "4.3.x-dev" }, diff --git a/Houdini/tests/HoudiniControllerTest.php b/Houdini/tests/HoudiniControllerTest.php index 9614ebb7..47607cac 100644 --- a/Houdini/tests/HoudiniControllerTest.php +++ b/Houdini/tests/HoudiniControllerTest.php @@ -30,7 +30,12 @@ class HoudiniControllerTest extends TestCase public function setUp(): void { parent::setUp(); - $this->mock_service = $this->prophesize(CmdExecuteService::class)->reveal(); + $prophecy = $this->prophesize(CmdExecuteService::class); + $prophecy->execute(Argument::any(), Argument::any()) + ->willReturn(function () { + return null; + }); + $this->mock_service = $prophecy->reveal(); $this->mock_logger = $this->prophesize(Logger::class)->reveal(); } diff --git a/Hypercube/.env b/Hypercube/.env index 6294ea9a..984aa735 100644 --- a/Hypercube/.env +++ b/Hypercube/.env @@ -9,9 +9,10 @@ # Real environment variables win over .env files. # # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/5.4/configuration/secrets.html # # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration +# https://symfony.com/doc/5.4/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### APP_ENV=dev @@ -19,3 +20,9 @@ APP_SECRET=803bbdd63ea73e87e7909327c60ce74a #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=17630295bdb1e8400b3260207d589c9b +###< lexik/jwt-authentication-bundle ### diff --git a/Hypercube/.gitignore b/Hypercube/.gitignore index a0546673..8271fd16 100644 --- a/Hypercube/.gitignore +++ b/Hypercube/.gitignore @@ -20,3 +20,7 @@ .phpunit.result.cache /phpunit.xml ###< symfony/phpunit-bridge ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### diff --git a/Hypercube/README.md b/Hypercube/README.md index 6b004fcc..c050b1f0 100644 --- a/Hypercube/README.md +++ b/Hypercube/README.md @@ -53,8 +53,7 @@ If your `pdftotext` installation is not on your path, then you can configure Hyp the `app.pdftotext_executable` parameter in [`/path/to/Hypercube/config/services.yaml`](./config/services.yaml). You also need to set your Fedora Base Url to allow the Fedora Resource to be pulled in automatically. This is done in the -`/path/to/Hypercube/config/packages/crayfish_commons.yaml`. In the same file you can point to the location of your `syn-settings.xml`. -If you don't have a `syn-settings.xml` look at the [Syn](http://github.com/Islandora/Syn) documentation. +`/path/to/Hypercube/config/packages/crayfish_commons.yaml`. In order to work on larger images, be sure `post_max_size` is sufficiently large and `max_execution_time` is set to 0 in your PHP installation's ini file. You can determine which ini file is getting used by running the command `$ php --ini`. @@ -69,10 +68,13 @@ environment directory will take precedence over those in the `/path/to/Hypercube The location specified in the configuration file for the log must be writable by the web server. -### Disabling Syn +### Enabling JWT authentication There are instructions in the `/path/to/Hypercube/config/packages/security.yaml` file describing what to change and what lines -to comment out to disable Syn. +to comment out to enable authentication. + +We use the Lexik JWT Authentication Bundle for Symfony, more information here +https://github.com/lexik/LexikJWTAuthenticationBundle ## Usage diff --git a/Hypercube/UPGRADE.md b/Hypercube/UPGRADE.md index f38bbe7c..fb45e033 100644 --- a/Hypercube/UPGRADE.md +++ b/Hypercube/UPGRADE.md @@ -1,5 +1,38 @@ This document guides you through the process of upgrading Hypercube. First, check if a section named "Upgrade to x.x.x" exists, with x.x.x being the version you are planning to upgrade to. +## Upgrade to 4.0.0 + +4.0.0 uses Symfony 5.4, which has differences from 3.x.x which used Symfony 4.4. +This makes it non-backwards compatible and requires testing of any custom changes you +may have made. + +Hypercube relies on Crayfish-Commons `^4.0` which no longer includes our own JWT authentication, +to perform JWT authentication we use the [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle). + +### Upgrade from version 3.x.x + +You can remove the `syn_config:` line from the `./config/packages/crayfish_commons.yaml` file. + +You will need to make a file in `./config/packages` called `lexik_jwt_authentication.yaml`. + +The file needs to contain (at a minimum) or you can copy the file from the Github repository: +```yaml +lexik_jwt_authentication: + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub +``` + +You can either: +* define an [environment variables](https://symfony.com/doc/5.4/configuration.html#configuration-based-on-environment-variables) for + the `JWT_PUBLIC_KEY` variable defined above and pointed to the Drupal public key file + +_or_ +* explicitly overwrite the `'%env(resolve:JWT_PUBLIC_KEY)%'` in + the above file and specify the path to the Drupal public key + + ## Upgrade to 3.0.0 Hypercube (and all of Crayfish) adheres to [semantic versioning](https://semver.org), which makes a distinction between "major", "minor", and "patch" versions. The upgrade path will be different depending on which previous version from which you are migrating. diff --git a/Hypercube/composer.json b/Hypercube/composer.json index fe0a8f92..bad94357 100644 --- a/Hypercube/composer.json +++ b/Hypercube/composer.json @@ -23,20 +23,24 @@ "require": { "ext-ctype": "*", "ext-iconv": "*", - "islandora/crayfish-commons": "^3.0", - "symfony/dotenv": "4.4.*", + "islandora/crayfish-commons": "^4.0", + "lexik/jwt-authentication-bundle": "^2.18", + "symfony/dotenv": "5.4.*", "symfony/flex": "^1.3.1", - "symfony/framework-bundle": "4.4.*", - "symfony/yaml": "4.4.*" + "symfony/framework-bundle": "5.4.*", + "symfony/runtime": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/yaml": "5.4.*" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "sebastian/phpcpd": "^6.0", "squizlabs/php_codesniffer": "^3.0", - "symfony/browser-kit": "4.4.*", - "symfony/css-selector": "4.4.*", - "symfony/phpunit-bridge": "4.4.*" + "symfony/browser-kit": "5.4.*", + "symfony/css-selector": "5.4.*", + "symfony/phpunit-bridge": "5.4.*" }, "minimum-stability": "dev", "prefer-stable": true, @@ -46,7 +50,8 @@ }, "sort-packages": true, "allow-plugins": { - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": true } }, "autoload": { @@ -83,7 +88,7 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "4.4.*" + "require": "5.4.*" } } } diff --git a/Hypercube/config/bootstrap.php b/Hypercube/config/bootstrap.php deleted file mode 100644 index 55560fb8..00000000 --- a/Hypercube/config/bootstrap.php +++ /dev/null @@ -1,23 +0,0 @@ -=1.2) -if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { - (new Dotenv(false))->populate($env); -} else { - // load all the .env files - (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); -} - -$_SERVER += $_ENV; -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/Hypercube/config/bundles.php b/Hypercube/config/bundles.php index 17f89aa1..b0a3082c 100644 --- a/Hypercube/config/bundles.php +++ b/Hypercube/config/bundles.php @@ -5,4 +5,5 @@ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Islandora\Crayfish\Commons\CrayfishCommonsBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; diff --git a/Hypercube/config/packages/crayfish_commons.yaml b/Hypercube/config/packages/crayfish_commons.yaml index 92c45c3e..6ebda51b 100644 --- a/Hypercube/config/packages/crayfish_commons.yaml +++ b/Hypercube/config/packages/crayfish_commons.yaml @@ -1,3 +1,2 @@ crayfish_commons: fedora_base_uri: 'http://localhost:8080/fcrepo/rest' - #syn_config: '/path/to/syn-settings.xml' diff --git a/Hypercube/config/packages/framework.yaml b/Hypercube/config/packages/framework.yaml index cad7f780..a375c3e7 100644 --- a/Hypercube/config/packages/framework.yaml +++ b/Hypercube/config/packages/framework.yaml @@ -1,8 +1,8 @@ -# see https://symfony.com/doc/current/reference/configuration/framework.html +# see https://symfony.com/doc/5.4/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' #csrf_protection: true - #http_method_override: true + http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. @@ -10,8 +10,15 @@ framework: handler_id: null cookie_secure: auto cookie_samesite: lax + storage_factory_id: session.storage.factory.native #esi: true #fragments: true php_errors: log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/Hypercube/config/packages/lexik_jwt_authentication.yaml b/Hypercube/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 00000000..c7a868e8 --- /dev/null +++ b/Hypercube/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,9 @@ +lexik_jwt_authentication: + # Need secret key to generate a token, this is not necessary for normal usage as the key is generated by Drupal. + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + # This is required if you have set a passphrase on the secret key, this is generally not needed. + pass_phrase: '%env(resolve:JWT_PASSPHRASE)%' + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub diff --git a/Hypercube/config/packages/security.yaml b/Hypercube/config/packages/security.yaml index f96d6af1..7dc90667 100644 --- a/Hypercube/config/packages/security.yaml +++ b/Hypercube/config/packages/security.yaml @@ -1,9 +1,10 @@ security: - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + enable_authenticator_manager: true + # https://symfony.com/doc/5.4/security.html#where-do-users-come-from-user-providers providers: users_in_memory: { memory: null } - jwt_user_provider: - id: Islandora\Crayfish\Commons\Syn\JwtUserProvider + jwt: + lexik_jwt: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -14,20 +15,18 @@ security: # Need stateless or it reloads the User based on a token. stateless: true - # To enable Syn, uncomment the below 4 lines and change anonymous to false above. - #provider: jwt_user_provider - #guard: - # authenticators: - # - Islandora\Crayfish\Commons\Syn\JwtAuthenticator + # To enable JWT authentication, uncomment the below 2 lines and change anonymous to false above. + #provider: jwt + #jwt: ~ # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#firewalls-authentication + # https://symfony.com/doc/5.4/security.html#firewalls-authentication - # https://symfony.com/doc/current/security/impersonating_user.html + # https://symfony.com/doc/5.4/security/impersonating_user.html # switch_user: true # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } diff --git a/Hypercube/config/preload.php b/Hypercube/config/preload.php index 064bdcd6..5ebcdb21 100644 --- a/Hypercube/config/preload.php +++ b/Hypercube/config/preload.php @@ -1,9 +1,5 @@ handle($request); -$response->send(); -$kernel->terminate($request, $response); +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/Hypercube/src/Controller/HypercubeController.php b/Hypercube/src/Controller/HypercubeController.php index 2e697091..2b799220 100644 --- a/Hypercube/src/Controller/HypercubeController.php +++ b/Hypercube/src/Controller/HypercubeController.php @@ -20,22 +20,22 @@ class HypercubeController /** * @var \Islandora\Crayfish\Commons\CmdExecuteService */ - protected $cmd; + protected CmdExecuteService $cmd; /** * @var string */ - protected $tesseract_executable; + protected string $tesseract_executable; /** * @var string */ - protected $pdftotext_executable; + protected string $pdftotext_executable; /** - * @var \Monolog\Logger + * @var \Psr\Log\LoggerInterface */ - protected $log; + protected LoggerInterface $log; /** * HypercubeController constructor. @@ -60,7 +60,7 @@ public function __construct( * @param \Symfony\Component\HttpFoundation\Request $request * @return \Symfony\Component\HttpFoundation\Response|\Symfony\Component\HttpFoundation\StreamedResponse */ - public function ocr(Request $request) + public function ocr(Request $request): Response { // Hack the fedora resource out of the attributes. $fedora_resource = $request->attributes->get('fedora_resource'); diff --git a/Hypercube/src/Kernel.php b/Hypercube/src/Kernel.php index 50b6fde2..8621c340 100644 --- a/Hypercube/src/Kernel.php +++ b/Hypercube/src/Kernel.php @@ -3,52 +3,9 @@ namespace App\Islandora\Hypercube; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Component\Routing\RouteCollectionBuilder; class Kernel extends BaseKernel { use MicroKernelTrait; - - private const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - public function registerBundles(): iterable - { - $contents = require $this->getProjectDir().'/config/bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - - public function getProjectDir(): string - { - return \dirname(__DIR__); - } - - protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void - { - $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); - $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); - } - - protected function configureRoutes(RouteCollectionBuilder $routes): void - { - $confDir = $this->getProjectDir().'/config'; - - $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); - } } diff --git a/Hypercube/symfony.lock b/Hypercube/symfony.lock index 7aadeab2..0d7fd4e0 100644 --- a/Hypercube/symfony.lock +++ b/Hypercube/symfony.lock @@ -20,6 +20,18 @@ "islandora/crayfish-commons": { "version": "3.0.0" }, + "lexik/jwt-authentication-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, "ml/iri": { "version": "1.1.4" }, @@ -289,18 +301,12 @@ "tests/bootstrap.php" ] }, - "symfony/polyfill-intl-idn": { - "version": "v1.23.0" - }, "symfony/polyfill-intl-normalizer": { "version": "v1.23.0" }, "symfony/polyfill-mbstring": { "version": "v1.23.1" }, - "symfony/polyfill-php72": { - "version": "v1.23.0" - }, "symfony/polyfill-php73": { "version": "v1.23.0" }, @@ -354,6 +360,19 @@ "symfony/service-contracts": { "version": "v2.4.0" }, + "symfony/translation": { + "version": "5.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/var-dumper": { "version": "v4.4.31" }, diff --git a/Hypercube/tests/HypercubeControllerTest.php b/Hypercube/tests/HypercubeControllerTest.php index cc7ec6e5..b6cd1d7a 100644 --- a/Hypercube/tests/HypercubeControllerTest.php +++ b/Hypercube/tests/HypercubeControllerTest.php @@ -124,6 +124,11 @@ private function mockController(bool $throwException = false): HypercubeControll if ($throwException) { $prophecy->execute(Argument::any(), Argument::any()) ->willThrow(new \RuntimeException("ERROR", 500)); + } else { + $prophecy->execute(Argument::any(), Argument::any()) + ->willReturn(function () { + return null; + }); } $mock_service = $prophecy->reveal(); $mock_logger = $this->prophesize(LoggerInterface::class)->reveal(); diff --git a/Milliner/.env b/Milliner/.env index 78e4d244..592a5698 100644 --- a/Milliner/.env +++ b/Milliner/.env @@ -9,9 +9,10 @@ # Real environment variables win over .env files. # # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/5.4/configuration/secrets.html # # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration +# https://symfony.com/doc/5.4/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### APP_ENV=dev @@ -19,3 +20,9 @@ APP_SECRET=2c4ca615eabc4764d2000e19eac23dc5 #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=6d4185246e75bf54a464f63b9aa3058f +###< lexik/jwt-authentication-bundle ### diff --git a/Milliner/README.md b/Milliner/README.md index e8172d6b..9df08eb1 100644 --- a/Milliner/README.md +++ b/Milliner/README.md @@ -46,10 +46,6 @@ resource has been updated more recently. `app.isFedora6` determines whether the Fedora instance is version 5.*.* or 6.*.*. -You do NOT need to edit the `fedora_base_url` inside `/path/to/Milliner/config/packages/crayfish_commons.yaml` as this -re-uses the above setting. However in the same file you can point to the location of your `syn-settings.xml`. -If you don't have a `syn-settings.xml` look at the [Syn](http://github.com/Islandora/Syn) documentation. - ### Logging To change your log settings, edit the `/path/to/Milliner/config/packages/monolog.yaml` file. @@ -60,10 +56,13 @@ environment directory will take precedence over those in the `/path/to/Milliner/ The location specified in the configuration file for the log must be writable by the web server. -### Disabling Syn +### Enabling JWT authentication There are instructions in the `/path/to/Milliner/config/packages/security.yaml` file describing what to change and what lines -to comment out to disable Syn. +to comment out to enable authentication. + +We use the Lexik JWT Authentication Bundle for Symfony, more information here +https://github.com/lexik/LexikJWTAuthenticationBundle ## Usage @@ -80,7 +79,7 @@ Milliner sets up a multiple endpoints, * `/external/{uuid}` which accepts POST requests. * POST creates a new external content resource for the Drupal resource -UUID is transformed into a Fedora URI using the [Crayfish-Commons](https://github.com/Islandora/Crayfish-Commons) EntityMapper. +UUID is transformed into a Fedora URI using the [Islandora to Fedora EntityMapper](https://github.com/Islandora/islandora-fedora-entity-mapper). ## Maintainers diff --git a/Milliner/UPGRADE.md b/Milliner/UPGRADE.md index 5a2a120e..486d852b 100644 --- a/Milliner/UPGRADE.md +++ b/Milliner/UPGRADE.md @@ -1,5 +1,39 @@ This document guides you through the process of upgrading Milliner. First, check if a section named "Upgrade to x.x.x" exists, with x.x.x being the version you are planning to upgrade to. +## Upgrade to 4.0.0 + +4.0.0 uses Symfony 5.4, which has differences from 3.x.x which used Symfony 4.4. +This makes it non-backwards compatible and requires testing of any custom changes you +may have made. + +### Upgrade from version 3.x.x +Milliner no longer relies on Crayfish-Commons, it now depends directly on [Chullo](https://github.com/Islandora/chullo) and +the [Islandora to Fedora EntityMapper](https://github.com/Islandora/islandora-fedora-entity-mapper). + +Milliner doesn't depend on Crayfish-Commons so to perform JWT authentication we use the [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle). + +You can remove the entire `./config/packages/crayfish_commons.yaml` file, just make sure you have +properly defined the `app.fedora_base_url` parameter in `./config/services.yaml` + +You will need to make a file in `./config/packages` called `lexik_jwt_authentication.yaml`. + +The file needs to contain (at a minimum) or you can copy the file from the Github repository: +```yaml +lexik_jwt_authentication: + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub +``` + +You can either: +* define an [environment variables](https://symfony.com/doc/5.4/configuration.html#configuration-based-on-environment-variables) for +the `JWT_PUBLIC_KEY` variable defined above and pointed to the Drupal public key file + +_or_ +* explicitly overwrite the `'%env(resolve:JWT_PUBLIC_KEY)%'` in +the above file and specify the path to the Drupal public key + ## Upgrade to 3.0.0 Milliner (and all of Crayfish) adheres to [semantic versioning](https://semver.org), which makes a distinction between "major", "minor", and "patch" versions. The upgrade path will be different depending on which previous version from which you are migrating. diff --git a/Milliner/composer.json b/Milliner/composer.json index 5fd143b0..8816b55c 100644 --- a/Milliner/composer.json +++ b/Milliner/composer.json @@ -5,20 +5,27 @@ "require": { "ext-ctype": "*", "ext-iconv": "*", - "islandora/crayfish-commons": "^3.0", - "symfony/dotenv": "4.4.*", + "islandora/chullo": "^2.0", + "islandora/fedora-entity-mapper": "^1.0", + "lexik/jwt-authentication-bundle": "^2.18", + "symfony/dotenv": "5.4.*", "symfony/flex": "^1.3.1", - "symfony/framework-bundle": "4.4.*", - "symfony/yaml": "4.4.*" + "symfony/framework-bundle": "5.4.*", + "symfony/monolog-bundle": "^3.4", + "symfony/runtime": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/yaml": "5.4.*" }, "require-dev": { + "donatj/mock-webserver": "^2.6", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "sebastian/phpcpd": "^6.0", "squizlabs/php_codesniffer": "^3.0", - "symfony/browser-kit": "4.4.*", - "symfony/css-selector": "4.4.*", - "symfony/phpunit-bridge": "4.4.*" + "symfony/browser-kit": "5.4.*", + "symfony/css-selector": "5.4.*", + "symfony/phpunit-bridge": "5.4.*" }, "authors": [ { @@ -45,7 +52,8 @@ }, "sort-packages": true, "allow-plugins": { - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": true } }, "autoload": { @@ -82,7 +90,7 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "4.4.*" + "require": "5.4.*" } } } diff --git a/Milliner/config/bootstrap.php b/Milliner/config/bootstrap.php deleted file mode 100644 index 55560fb8..00000000 --- a/Milliner/config/bootstrap.php +++ /dev/null @@ -1,23 +0,0 @@ -=1.2) -if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { - (new Dotenv(false))->populate($env); -} else { - // load all the .env files - (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); -} - -$_SERVER += $_ENV; -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/Milliner/config/bundles.php b/Milliner/config/bundles.php index 17f89aa1..f2107486 100644 --- a/Milliner/config/bundles.php +++ b/Milliner/config/bundles.php @@ -4,5 +4,5 @@ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], - Islandora\Crayfish\Commons\CrayfishCommonsBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; diff --git a/Milliner/config/packages/crayfish_commons.yaml b/Milliner/config/packages/crayfish_commons.yaml deleted file mode 100644 index 5994e748..00000000 --- a/Milliner/config/packages/crayfish_commons.yaml +++ /dev/null @@ -1,4 +0,0 @@ -crayfish_commons: - # Because we define a Fedora parameter in the services.yaml we can re-use it here. - fedora_base_uri: '%app.fedora_base_url%' - #syn_config: '/path/to/syn-settings.xml' diff --git a/Milliner/config/packages/framework.yaml b/Milliner/config/packages/framework.yaml index cad7f780..a375c3e7 100644 --- a/Milliner/config/packages/framework.yaml +++ b/Milliner/config/packages/framework.yaml @@ -1,8 +1,8 @@ -# see https://symfony.com/doc/current/reference/configuration/framework.html +# see https://symfony.com/doc/5.4/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' #csrf_protection: true - #http_method_override: true + http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. @@ -10,8 +10,15 @@ framework: handler_id: null cookie_secure: auto cookie_samesite: lax + storage_factory_id: session.storage.factory.native #esi: true #fragments: true php_errors: log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/Milliner/config/packages/lexik_jwt_authentication.yaml b/Milliner/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 00000000..c7a868e8 --- /dev/null +++ b/Milliner/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,9 @@ +lexik_jwt_authentication: + # Need secret key to generate a token, this is not necessary for normal usage as the key is generated by Drupal. + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + # This is required if you have set a passphrase on the secret key, this is generally not needed. + pass_phrase: '%env(resolve:JWT_PASSPHRASE)%' + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub diff --git a/Milliner/config/packages/security.yaml b/Milliner/config/packages/security.yaml index 284b7a30..7dc90667 100644 --- a/Milliner/config/packages/security.yaml +++ b/Milliner/config/packages/security.yaml @@ -1,9 +1,10 @@ security: - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + enable_authenticator_manager: true + # https://symfony.com/doc/5.4/security.html#where-do-users-come-from-user-providers providers: users_in_memory: { memory: null } - jwt_user_provider: - id: Islandora\Crayfish\Commons\Syn\JwtUserProvider + jwt: + lexik_jwt: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -14,16 +15,14 @@ security: # Need stateless or it reloads the User based on a token. stateless: true - # To enable Syn, uncomment the below 4 lines and change anonymous to false above. - #provider: jwt_user_provider - #guard: - # authenticators: - # - Islandora\Crayfish\Commons\Syn\JwtAuthenticator + # To enable JWT authentication, uncomment the below 2 lines and change anonymous to false above. + #provider: jwt + #jwt: ~ # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#firewalls-authentication + # https://symfony.com/doc/5.4/security.html#firewalls-authentication - # https://symfony.com/doc/current/security/impersonating_user.html + # https://symfony.com/doc/5.4/security/impersonating_user.html # switch_user: true # Easy way to control access for large sections of your site diff --git a/Milliner/config/preload.php b/Milliner/config/preload.php index 064bdcd6..5ebcdb21 100644 --- a/Milliner/config/preload.php +++ b/Milliner/config/preload.php @@ -1,9 +1,5 @@ handle($request); -$response->send(); -$kernel->terminate($request, $response); +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/Milliner/src/Controller/MillinerController.php b/Milliner/src/Controller/MillinerController.php index 9a9d1bfc..b8b63f8e 100644 --- a/Milliner/src/Controller/MillinerController.php +++ b/Milliner/src/Controller/MillinerController.php @@ -17,12 +17,12 @@ class MillinerController /** * @var \App\Islandora\Milliner\Service\MillinerServiceInterface */ - protected $milliner; + protected MillinerServiceInterface $milliner; /** * @var \Psr\Log\LoggerInterface */ - protected $log; + protected LoggerInterface $log; /** * MillinerController constructor. @@ -41,9 +41,9 @@ public function __construct(MillinerServiceInterface $milliner, LoggerInterface * @param \Symfony\Component\HttpFoundation\Request $request * The request. * @return \Symfony\Component\HttpFoundation\Response - * A response + * The response generated from the response from Fedora. */ - public function saveNode($uuid, Request $request): Response + public function saveNode(string $uuid, Request $request): Response { $token = $request->headers->get("Authorization", null); $jsonld_url = $request->headers->get("Content-Location"); @@ -76,10 +76,13 @@ public function saveNode($uuid, Request $request): Response /** * @param string $uuid + * The UUID of the Drupal resource to delete. * @param \Symfony\Component\HttpFoundation\Request $request + * The request. * @return \Symfony\Component\HttpFoundation\Response + * The response generated from the response from Fedora. */ - public function deleteNode($uuid, Request $request) + public function deleteNode(string $uuid, Request $request): Response { $token = $request->headers->get("Authorization", null); $islandora_fedora_endpoint = $request->headers->get("X-Islandora-Fedora-Endpoint"); @@ -104,10 +107,13 @@ public function deleteNode($uuid, Request $request) /** * @param string $source_field + * The source field of the media being saved. * @param \Symfony\Component\HttpFoundation\Request $request + * The request. * @return \Symfony\Component\HttpFoundation\Response + * The response generated from the response from Fedora. */ - public function saveMedia($source_field, Request $request) + public function saveMedia(string $source_field, Request $request): Response { $token = $request->headers->get("Authorization", null); $json_url = $request->headers->get("Content-Location"); @@ -138,10 +144,13 @@ public function saveMedia($source_field, Request $request) /** * @param string $uuid + * The UUID of the Drupal resource to save as external. * @param \Symfony\Component\HttpFoundation\Request $request + * The request. * @return \Symfony\Component\HttpFoundation\Response + * The response generated from the response from Fedora. */ - public function saveExternal($uuid, Request $request) + public function saveExternal(string $uuid, Request $request): Response { $token = $request->headers->get("Authorization", null); $external_url = $request->headers->get("Content-Location"); @@ -172,10 +181,13 @@ public function saveExternal($uuid, Request $request) /** * @param string $uuid + * The UUID of the Drupal resource to create a version of. * @param \Symfony\Component\HttpFoundation\Request $request + * The request. * @return \Symfony\Component\HttpFoundation\Response + * The response generated from the response from Fedora. */ - public function createNodeVersion($uuid, Request $request) + public function createNodeVersion(string $uuid, Request $request): Response { $token = $request->headers->get("Authorization", null); $islandora_fedora_endpoint = $request->headers->get("X-Islandora-Fedora-Endpoint"); @@ -199,10 +211,13 @@ public function createNodeVersion($uuid, Request $request) /** * @param string $source_field + * The source field of the Drupal resource to create a version of media. * @param \Symfony\Component\HttpFoundation\Request $request + * The request. * @return \Symfony\Component\HttpFoundation\Response + * The response generated from the response from Fedora. */ - public function createMediaVersion($source_field, Request $request) + public function createMediaVersion(string $source_field, Request $request): Response { $token = $request->headers->get("Authorization", null); $json_url = $request->headers->get("Content-Location"); diff --git a/Milliner/src/Kernel.php b/Milliner/src/Kernel.php index 4a84b226..4985502f 100644 --- a/Milliner/src/Kernel.php +++ b/Milliner/src/Kernel.php @@ -3,52 +3,9 @@ namespace App\Islandora\Milliner; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Component\Routing\RouteCollectionBuilder; class Kernel extends BaseKernel { use MicroKernelTrait; - - private const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - public function registerBundles(): iterable - { - $contents = require $this->getProjectDir().'/config/bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - - public function getProjectDir(): string - { - return \dirname(__DIR__); - } - - protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void - { - $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); - $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); - } - - protected function configureRoutes(RouteCollectionBuilder $routes): void - { - $confDir = $this->getProjectDir().'/config'; - - $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); - } } diff --git a/Milliner/src/Service/MillinerService.php b/Milliner/src/Service/MillinerService.php index fdedd69f..da483c40 100644 --- a/Milliner/src/Service/MillinerService.php +++ b/Milliner/src/Service/MillinerService.php @@ -8,8 +8,8 @@ use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Psr7\Header; use GuzzleHttp\Psr7\Response; -use Islandora\Chullo\IFedoraApi; -use Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface; +use Islandora\Chullo\FedoraApi; +use Islandora\EntityMapper\EntityMapper; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; @@ -33,7 +33,7 @@ class MillinerService implements MillinerServiceInterface /** * Entity path mapper instance. - * @var \Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface + * @var \Islandora\EntityMapper\EntityMapperInterface */ protected $mapper; @@ -64,14 +64,12 @@ class MillinerService implements MillinerServiceInterface /** * MillinerService constructor. * - * @param \Islandora\Chullo\IFedoraApi $fedora - * Fedora client. * @param \GuzzleHttp\Client $drupal * Http client for Drupal. - * @param \Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface $mapper - * Entity mapper. * @param \Psr\Log\LoggerInterface $log * Logger + * @param string $fedoraBaseUrl + * The base url to Fedora * @param string $modifiedDatePredicate * The modified date predicate to use for comparing last altered datetimes. * @param bool $stripFormatJsonld @@ -80,31 +78,30 @@ class MillinerService implements MillinerServiceInterface * Whether the Fedora we are pointing at is Fedora 6. */ public function __construct( - IFedoraApi $fedora, Client $drupal, - EntityMapperInterface $mapper, LoggerInterface $log, + string $fedoraBaseUrl, string $modifiedDatePredicate, bool $stripFormatJsonld, bool $isFedora6 ) { - $this->fedora = $fedora; + $this->fedora = FedoraApi::create($fedoraBaseUrl); $this->drupal = $drupal; - $this->mapper = $mapper; $this->log = $log; $this->modifiedDatePredicate = $modifiedDatePredicate; $this->stripFormatJsonld = $stripFormatJsonld; $this->isFedora6 = $isFedora6; + $this->mapper = new EntityMapper(); } /** - * {@inheritdoc} + * @inheritDoc */ public function saveNode( string $uuid, string $jsonld_url, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface { $path = $this->mapper->getFedoraPath($uuid); $islandora_fedora_endpoint = rtrim($islandora_fedora_endpoint, "/"); @@ -131,11 +128,11 @@ public function saveNode( /** * Creates a new LDP-RS in Fedora from a Node. * - * @param string $jsonld_url - * @param string $fedora_url - * @param string|null $token + * @param string $jsonld_url The Drupal Json-LD ID of the resource. + * @param string $fedora_url The Fedora ID of the associated resource. + * @param string|null $token The JWT token or null if none. * - * @return \Psr\Http\Message\ResponseInterface + * @return \Psr\Http\Message\ResponseInterface The response from the chullo saveResource call to Fedora. * * @throws \RuntimeException * @throws \GuzzleHttp\Exception\RequestException @@ -143,8 +140,8 @@ public function saveNode( protected function createNode( string $jsonld_url, string $fedora_url, - string $token = null - ) { + ?string $token = null + ): ResponseInterface { // Get the jsonld from Drupal. $headers = empty($token) ? [] : ['Authorization' => $token]; $drupal_response = $this->drupal->get( @@ -193,11 +190,11 @@ protected function createNode( /** * Updates an existing LDP-RS in Fedora from a Node. * - * @param string $jsonld_url - * @param string $fedora_url - * @param string $token + * @param string $jsonld_url The Drupal Json-LD ID of the resource. + * @param string $fedora_url The Fedora ID of the associated resource. + * @param string|null $token The JWT token or null if none. * - * @return \Psr\Http\Message\ResponseInterface + * @return \Psr\Http\Message\ResponseInterface The response from the chullo saveResource call to Fedora. * * @throws \RuntimeException * @throws \GuzzleHttp\Exception\RequestException @@ -205,8 +202,8 @@ protected function createNode( protected function updateNode( string $jsonld_url, string $fedora_url, - string $token = null - ) { + ?string $token = null + ): ResponseInterface { // Get the RDF from Fedora. $headers = empty($token) ? [] : ['Authorization' => $token]; $headers['Accept'] = 'application/ld+json'; @@ -315,13 +312,13 @@ protected function updateNode( /** * Normalizes Drupal jsonld into a shape Fedora understands. * - * @param array $jsonld - * @param string $drupal_url - * @param string $fedora_url + * @param array $jsonld The Json-LD array. + * @param string $drupal_url The Drupal URL. + * @param string $fedora_url The Fedora URL. * - * @return array + * @return array The processed Json-LD array */ - protected function processJsonld(array $jsonld, $drupal_url, $fedora_url) + protected function processJsonld(array $jsonld, string $drupal_url, string $fedora_url): array { $this->log->debug("DRUPAL URL: $drupal_url"); $this->log->debug("FEDORA URL: $fedora_url"); @@ -350,13 +347,13 @@ function (array $elem) use ($subject_url) { /** * Gets the first value for a predicate in a JSONLD array. * - * @param $jsonld - * @param $predicate - * @param $value + * @param array $jsonld The Json-LD array. + * @param string $predicate the predicate to look for. + * @param bool $value Is this a @value and not an @id. * - * @return mixed string|null + * @return string|null */ - protected function getFirstPredicate(array $jsonld, $predicate, $value = true) + protected function getFirstPredicate(array $jsonld, string $predicate, bool $value = true): ?string { $key = $value ? '@value' : '@id'; $malformed = empty($jsonld) || @@ -380,7 +377,7 @@ protected function getFirstPredicate(array $jsonld, $predicate, $value = true) * * @throws \RuntimeException */ - protected function getModifiedTimestamp(array $jsonld) + protected function getModifiedTimestamp(array $jsonld): int { $modified = $this->getFirstPredicate( $jsonld, @@ -403,7 +400,7 @@ protected function getModifiedTimestamp(array $jsonld) } /** - * {@inheritDoc} + * @inheritDoc */ public function saveMedia( $source_field, @@ -446,7 +443,7 @@ protected function getLinkHeader(ResponseInterface $response, string $rel_name, } /** - * {@inheritDoc} + * @inheritDoc */ public function deleteNode( $uuid, diff --git a/Milliner/src/Service/MillinerServiceInterface.php b/Milliner/src/Service/MillinerServiceInterface.php index 3378beec..0e9745f5 100644 --- a/Milliner/src/Service/MillinerServiceInterface.php +++ b/Milliner/src/Service/MillinerServiceInterface.php @@ -28,7 +28,7 @@ public function saveNode( string $uuid, string $jsonld_url, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface; /** @@ -49,7 +49,7 @@ public function saveMedia( string $source_field, string $json_url, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface; /** @@ -67,7 +67,7 @@ public function saveMedia( public function deleteNode( string $uuid, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface; /** @@ -88,7 +88,7 @@ public function saveExternal( string $uuid, string $external_url, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface; /** @@ -106,7 +106,7 @@ public function saveExternal( public function createVersion( string $uuid, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface; /** @@ -127,6 +127,6 @@ public function createMediaVersion( string $source_field, string $json_url, string $islandora_fedora_endpoint, - string $token = null + ?string $token = null ): ResponseInterface; } diff --git a/Milliner/symfony.lock b/Milliner/symfony.lock index 7aadeab2..43ca5276 100644 --- a/Milliner/symfony.lock +++ b/Milliner/symfony.lock @@ -17,8 +17,17 @@ "islandora/chullo": { "version": "1.3.0" }, - "islandora/crayfish-commons": { - "version": "3.0.0" + "lexik/jwt-authentication-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] }, "ml/iri": { "version": "1.1.4" @@ -289,18 +298,12 @@ "tests/bootstrap.php" ] }, - "symfony/polyfill-intl-idn": { - "version": "v1.23.0" - }, "symfony/polyfill-intl-normalizer": { "version": "v1.23.0" }, "symfony/polyfill-mbstring": { "version": "v1.23.1" }, - "symfony/polyfill-php72": { - "version": "v1.23.0" - }, "symfony/polyfill-php73": { "version": "v1.23.0" }, @@ -354,6 +357,19 @@ "symfony/service-contracts": { "version": "v2.4.0" }, + "symfony/translation": { + "version": "5.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/var-dumper": { "version": "v4.4.31" }, diff --git a/Milliner/tests/AbstractMillinerTestCase.php b/Milliner/tests/AbstractMillinerTestCase.php index a7818dcb..db7d49e8 100644 --- a/Milliner/tests/AbstractMillinerTestCase.php +++ b/Milliner/tests/AbstractMillinerTestCase.php @@ -4,10 +4,9 @@ use App\Islandora\Milliner\Service\MillinerService; use App\Islandora\Milliner\Service\MillinerServiceInterface; +use donatj\MockWebServer\MockWebServer; use GuzzleHttp\Client; -use GuzzleHttp\Psr7\Response; -use Islandora\Chullo\IFedoraApi; -use Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface; +use Islandora\EntityMapper\EntityMapper; use Monolog\Handler\NullHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; @@ -31,117 +30,163 @@ class AbstractMillinerTestCase extends TestCase * The predicate to compare when checking date modified. * @var string */ - protected $modifiedDatePredicate; + protected string $modifiedDatePredicate; /** * @var string */ - protected $uuid; + protected string $uuid; /** * Is the Fedora of version >= 6.0.0 * @var bool */ - protected $isFedora6 = false; + protected bool $isFedora6 = false; /** * Whether to strip the ?_format=jsonld from URLs * @var bool */ - protected $stripJsonLd = false; + protected bool $stripJsonLd = false; /** * @var string */ - protected $fedoraBaseUrl; + protected string $fedoraBaseUrl; /** - * @var \Islandora\Chullo\IFedoraApi + * @var \Islandora\Chullo\IFedoraApi|\Prophecy\Prophecy\ObjectProphecy */ protected $fedora_client_prophecy; /** - * @var \Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface - */ - protected $entity_mapper_prophecy; - - /** - * @var \GuzzleHttp\Client + * @var \GuzzleHttp\Client|\Prophecy\Prophecy\ObjectProphecy */ protected $drupal_client_prophecy; /** * A 200 OK response. - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response */ - protected $ok_response; + protected \donatj\MockWebServer\Response $ok_response; /** * A 201 Created response. - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response */ - protected $created_response; + protected \donatj\MockWebServer\Response $created_response; /** * A 204 No Content response. - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response */ - protected $no_content_response; + protected \donatj\MockWebServer\Response $no_content_response; /** * A 404 Not Found response. - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response */ - protected $not_found_response; + protected \donatj\MockWebServer\Response $not_found_response; /** * A 401 Unauthorized response. - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response */ - protected $unauthorized_response; + protected \donatj\MockWebServer\Response $unauthorized_response; /** * A 403 Forbidden response - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response */ - protected $forbidden_response; + protected \donatj\MockWebServer\Response $forbidden_response; /** * A 410 Gone response - * @var \GuzzleHttp\Psr7\Response + * @var \donatj\MockWebServer\Response + */ + protected \donatj\MockWebServer\Response $gone_response; + + /** + * The mock webserver for Fedora responses. + * @var \donatj\MockWebServer\MockWebServer + */ + protected static $webserver; + + /** + * An entity mapper. + * @var \Islandora\EntityMapper\EntityMapper + */ + protected EntityMapper $entity_mapper; + + /** + * The mapped Drupal UUID as a fedora path. + * @var string + */ + protected string $fedora_path; + + /** + * The full Fedora URI. + * @var string */ - protected $gone_response; + protected string $fedora_full_uri; /** - * {@inheritDoc} + * @inheritDoc + */ + public static function setUpBeforeClass(): void + { + self::$webserver = new MockWebServer(); + self::$webserver->start(); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + self::$webserver->stop(); + } + + /** + * @inheritDoc */ protected function setUp(): void { parent::setUp(); $this->logger = new Logger('milliner'); $this->logger->pushHandler(new NullHandler()); + $this->entity_mapper = new EntityMapper(); // Default properties $this->modifiedDatePredicate = "http://schema.org/dateModified"; - $this->fedoraBaseUrl = 'http://localhost:8080/fcrepo/rest'; - $this->uuid = '9541c0c1-5bee-4973-a9d0-e55c1658bc8'; + $this->fedoraBaseUrl = 'http://' . self::$webserver->getHost() . ':' . + self::$webserver->getPort() . '/fcrepo/rest'; + $this->rebuildFedoraUris('9541c0c1-5bee-4973-a9d0-e55c1658bc8'); // Prophecies $this->drupal_client_prophecy = $this->prophesize(Client::class); - $this->entity_mapper_prophecy = $this->prophesize(EntityMapperInterface::class); - $this->fedora_client_prophecy = $this->prophesize(IFedoraApi::class); - - $this->entity_mapper_prophecy->getFedoraPath($this->uuid) - ->willReturn("{$this->fedoraBaseUrl}/95/41/c0/c1/9541c0c1-5bee-4973-a9d0-e55c1658bc8"); // Reusable responses - $this->ok_response = new Response(200); - $this->created_response = new Response(201); - $this->no_content_response = new Response(204); - $this->not_found_response = new Response(404); - $this->forbidden_response = new Response(403, [], null, '1.1', 'FORBIDDEN'); - $this->unauthorized_response = new Response(401, [], null, '1.1', 'UNAUTHORIZED'); - $this->gone_response = new Response(410); + $this->ok_response = new \donatj\MockWebServer\Response("", [], 200); + $this->created_response = new \donatj\MockWebServer\Response("", [], 201); + $this->no_content_response = new \donatj\MockWebServer\Response("", [], 204); + $this->not_found_response = new \donatj\MockWebServer\Response("", [], 404); + $this->forbidden_response = new \donatj\MockWebServer\Response("", [], 403); + $this->unauthorized_response = new \donatj\MockWebServer\Response("", [], 401); + $this->gone_response = new \donatj\MockWebServer\Response("", [], 410); + } + + /** + * Rebuild all the variables for Fedora URIs based on this UUID. + * @param string $uuid the UUID + * @return void + */ + protected function rebuildFedoraUris(string $uuid): void + { + $this->uuid = $uuid; + $mapped_path = $this->entity_mapper->getFedoraPath($this->uuid); + $this->fedora_path = '/fcrepo/rest/' . $mapped_path; + $this->fedora_full_uri = "{$this->fedoraBaseUrl}/$mapped_path"; } /** @@ -163,10 +208,9 @@ protected function getStaticFile(string $filename): string protected function getMilliner(): MillinerServiceInterface { return new MillinerService( - $this->fedora_client_prophecy->reveal(), $this->drupal_client_prophecy->reveal(), - $this->entity_mapper_prophecy->reveal(), $this->logger, + $this->fedoraBaseUrl, $this->modifiedDatePredicate, $this->stripJsonLd, $this->isFedora6 diff --git a/Milliner/tests/CreateVersionTest.php b/Milliner/tests/CreateVersionTest.php index 7ed5dbf4..fda750ce 100644 --- a/Milliner/tests/CreateVersionTest.php +++ b/Milliner/tests/CreateVersionTest.php @@ -2,7 +2,8 @@ namespace App\Islandora\Milliner\Tests; -use Prophecy\Argument; +use donatj\MockWebServer\Response; +use donatj\MockWebServer\ResponseByMethod; /** * Class MillinerServiceTest @@ -18,9 +19,22 @@ class CreateVersionTest extends AbstractMillinerTestCase */ public function testCreateVersionReturnsFedora201() { - - $this->fedora_client_prophecy->createVersion(Argument::any(), Argument::any(), Argument::any(), Argument::any()) - ->willReturn($this->created_response); + self::$webserver->setResponseOfPath( + $this->fedora_path . '/fcr:versions', + new ResponseByMethod([ + ResponseByMethod::METHOD_POST => $this->created_response + ]) + ); + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod([ + ResponseByMethod::METHOD_HEAD => new Response( + '', + ['Link' => '<' . $this->fedora_full_uri . '/fcr:versions>; rel="timemap"'], + 200 + ) + ]) + ); $milliner = $this->getMilliner(); @@ -44,11 +58,14 @@ public function testCreateVersionReturnsFedora201() public function testCreateVersionReturnsFedora404() { - $this->fedora_client_prophecy->createVersion(Argument::any(), Argument::any(), Argument::any(), Argument::any()) - ->willReturn($this->not_found_response); + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod([ + ResponseByMethod::METHOD_HEAD => $this->not_found_response + ]) + ); $this->expectException(\RuntimeException::class); - $this->expectExceptionCode(404); $milliner = $this->getMilliner(); @@ -72,11 +89,14 @@ public function testCreateVersionReturnsFedora404() */ public function testcreateVersionThrowsOnFedoraSaveError() { - $this->fedora_client_prophecy->createVersion(Argument::any(), Argument::any(), Argument::any(), Argument::any()) - ->willReturn($this->forbidden_response); + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod([ + ResponseByMethod::METHOD_HEAD => $this->forbidden_response + ]) + ); $this->expectException(\RuntimeException::class); - $this->expectExceptionCode(403); $milliner = $this->getMilliner(); diff --git a/Milliner/tests/DeleteTest.php b/Milliner/tests/DeleteTest.php index a1f124eb..5268b191 100644 --- a/Milliner/tests/DeleteTest.php +++ b/Milliner/tests/DeleteTest.php @@ -2,7 +2,8 @@ namespace App\Islandora\Milliner\Tests; -use Prophecy\Argument; +use donatj\MockWebServer\ResponseByMethod; +use donatj\MockWebServer\ResponseStack; /** * Class MillinerServiceTest @@ -18,8 +19,12 @@ class DeleteTest extends AbstractMillinerTestCase */ public function testDeleteThrowsFedoraError() { - $this->fedora_client_prophecy->deleteResource(Argument::any(), Argument::any()) - ->willReturn($this->forbidden_response); + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod([ + ResponseByMethod::METHOD_DELETE => $this->forbidden_response, + ]) + ); $milliner = $this->getMilliner(); @@ -35,8 +40,20 @@ public function testDeleteThrowsFedoraError() */ public function testDeleteReturnsFedoraResult() { - $this->fedora_client_prophecy->deleteResource(Argument::any(), Argument::any()) - ->willReturn($this->no_content_response); + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseStack( + new ResponseByMethod([ + ResponseByMethod::METHOD_DELETE => $this->no_content_response, + ]), + new ResponseByMethod([ + ResponseByMethod::METHOD_DELETE => $this->not_found_response, + ]), + new ResponseByMethod([ + ResponseByMethod::METHOD_DELETE => $this->gone_response, + ]), + ) + ); $milliner = $this->getMilliner(); @@ -47,11 +64,6 @@ public function testDeleteReturnsFedoraResult() "Milliner must return 204 when Fedora returns 204. Received: $status" ); - $this->fedora_client_prophecy->deleteResource(Argument::any(), Argument::any()) - ->willReturn($this->not_found_response); - - $milliner = $this->getMilliner(); - $response = $milliner->deleteNode($this->uuid, $this->fedoraBaseUrl, "Bearer islandora"); $status = $response->getStatusCode(); $this->assertTrue( @@ -59,11 +71,6 @@ public function testDeleteReturnsFedoraResult() "Milliner must return 404 when Fedora returns 404. Received: $status" ); - $this->fedora_client_prophecy->deleteResource(Argument::any(), Argument::any()) - ->willReturn($this->gone_response); - - $milliner = $this->getMilliner(); - $response = $milliner->deleteNode($this->uuid, $this->fedoraBaseUrl, "Bearer islandora"); $status = $response->getStatusCode(); $this->assertTrue( diff --git a/Milliner/tests/SaveExternalTest.php b/Milliner/tests/SaveExternalTest.php index 0ea11e02..64dc8561 100644 --- a/Milliner/tests/SaveExternalTest.php +++ b/Milliner/tests/SaveExternalTest.php @@ -52,9 +52,10 @@ public function testSaveExternalThrowsOnPutError() { $this->drupal_client_prophecy->head(Argument::any(), Argument::any()) ->willReturn(new Response(200, ['Content-Type' => 'image/jpeg'])); - - $this->fedora_client_prophecy->saveResource(Argument::any(), Argument::any(), Argument::any()) - ->willReturn($this->forbidden_response); + self::$webserver->setResponseOfPath( + $this->fedora_path, + $this->forbidden_response + ); $milliner = $this->getMilliner(); diff --git a/Milliner/tests/SaveMediaTest.php b/Milliner/tests/SaveMediaTest.php index 04a83410..7a63ff4b 100644 --- a/Milliner/tests/SaveMediaTest.php +++ b/Milliner/tests/SaveMediaTest.php @@ -2,6 +2,7 @@ namespace App\Islandora\Milliner\Tests; +use donatj\MockWebServer\ResponseByMethod; use GuzzleHttp\Psr7\Response; use App\Islandora\Milliner\Service\MillinerService; use Prophecy\Argument; @@ -21,10 +22,7 @@ protected function setUp(): void { parent::setUp(); - $this->uuid = 'ffb15b4f-54db-44ce-ad0b-3588889a3c9b'; - - $this->entity_mapper_prophecy->getFedoraPath($this->uuid) - ->willReturn("{$this->fedoraBaseUrl}/ff/b1/5b/4f/ffb15b4f-54db-44ce-ad0b-3588889a3c9b"); + $this->rebuildFedoraUris('ffb15b4f-54db-44ce-ad0b-3588889a3c9b'); } /** @@ -217,13 +215,12 @@ public function testSaveMediaThrowsFedoraGetError() $this->drupal_client_prophecy->head(Argument::any(), Argument::any()) ->willReturn($head_response); - $fedora_get_response = new Response( - 404 + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod([ + ResponseByMethod::METHOD_GET => $this->not_found_response, + ]) ); - - $this->fedora_client_prophecy->getResource(Argument::any(), Argument::any()) - ->willReturn($fedora_get_response); - $milliner = $this->getMilliner(); $this->expectException(\RuntimeException::class, null, 404); @@ -274,14 +271,17 @@ public function testSaveMediaThrows412OnStaleData() $this->drupal_client_prophecy->head(Argument::any(), Argument::any()) ->willReturn($head_response); - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents($this->getStaticFile('MediaLDP-RS.jsonld')), ['Content-Type' => 'application/ld+json', 'ETag' => 'W\abc123'], - file_get_contents($this->getStaticFile('MediaLDP-RS.jsonld')) + 200 + ); + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod([ + ResponseByMethod::METHOD_GET => $fedora_get_response, + ]) ); - - $this->fedora_client_prophecy->getResource(Argument::any(), Argument::any()) - ->willReturn($fedora_get_response); $milliner = $this->getMilliner(); @@ -376,13 +376,15 @@ public function testSaveMediaReturnsNoModifiedDate() * * @param string $mediaResponseFilename * The file to use as the response to the Fedora request. - * @param Response $fedora_put_response + * @param \donatj\MockWebServer\Response $fedora_put_response * The response to return when attempting to PUT to Fedora. * * @return \App\Islandora\Milliner\Service\MillinerService */ - private function setupMillinerSave(string $mediaResponseFilename, Response $fedora_put_response): MillinerService - { + private function setupMillinerSave( + string $mediaResponseFilename, + \donatj\MockWebServer\Response $fedora_put_response + ): MillinerService { $link = '; rel="alternate"; type="application/ld+json"'; $link .= ',; rel="describes"'; $drupal_json_response = new Response( @@ -405,7 +407,7 @@ private function setupMillinerSave(string $mediaResponseFilename, Response $fedo $this->drupal_client_prophecy->get('http://localhost:8000/media/6?_format=jsonld', Argument::any()) ->willReturn($drupal_jsonld_response); - $link = ''; + $link = "<{$this->fedora_full_uri}/fcr:metadata>"; $link .= '; rel="describedby"'; $head_response = new Response( 200, @@ -414,25 +416,26 @@ private function setupMillinerSave(string $mediaResponseFilename, Response $fedo $this->drupal_client_prophecy->head(Argument::any(), Argument::any()) ->willReturn($head_response); - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents($this->getStaticFile($mediaResponseFilename)), ['Content-Type' => 'application/ld+json', 'ETag' => 'W\abc123'], - file_get_contents($this->getStaticFile($mediaResponseFilename)) + 200 + ); + // This is media tests so we need the responses to be at the + // fcr:metadata endpoint for metadata. + self::$webserver->setResponseOfPath( + $this->fedora_path . '/fcr:metadata', + new ResponseByMethod([ + ResponseByMethod::METHOD_GET => $fedora_get_response, + ResponseByMethod::METHOD_PUT => $fedora_put_response, + ]) ); - $this->fedora_client_prophecy->getResource(Argument::any(), Argument::any()) - ->willReturn($fedora_get_response); - $this->fedora_client_prophecy->saveResource(Argument::any(), Argument::any(), Argument::any()) - ->willReturn($fedora_put_response); - - $this->entity_mapper_prophecy->getFedoraPath('f0fd71b3-1fab-45e1-a5e9-78d50e0d7174') - ->willReturn("{$this->fedoraBaseUrl}/f0/fd/71/b3/f0fd71b3-1fab-45e1-a5e9-78d50e0d7174"); return new MillinerService( - $this->fedora_client_prophecy->reveal(), $this->drupal_client_prophecy->reveal(), - $this->entity_mapper_prophecy->reveal(), $this->logger, + $this->fedoraBaseUrl, $this->modifiedDatePredicate, false, false diff --git a/Milliner/tests/SaveNodeTest.php b/Milliner/tests/SaveNodeTest.php index 60c36671..afea4f8b 100644 --- a/Milliner/tests/SaveNodeTest.php +++ b/Milliner/tests/SaveNodeTest.php @@ -2,6 +2,7 @@ namespace App\Islandora\Milliner\Tests; +use donatj\MockWebServer\ResponseByMethod; use GuzzleHttp\Psr7\Response; use App\Islandora\Milliner\Service\MillinerService; use Prophecy\Argument; @@ -39,7 +40,7 @@ public function testCreateNodeThrowsOnFedoraError() { $milliner = $this->setupMilliner($this->not_found_response, null, $this->unauthorized_response); - $this->expectException(\RuntimeException::class, null, 403); + $this->expectException(\RuntimeException::class, null, 401); $milliner->saveNode( $this->uuid, @@ -107,10 +108,10 @@ public function testCreateNodeReturnsFedora204() */ public function testUpdateNodeThrowsOnFedoraError() { - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld'), ['Content-Type' => 'application/ld+json'], - file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld') + 200 ); $milliner = $this->setupMilliner($this->ok_response, $fedora_get_response, $this->unauthorized_response); @@ -143,10 +144,10 @@ public function testUpdateNodeThrows500OnBadDatePredicate() $this->drupal_client_prophecy->get(Argument::any(), Argument::any()) ->willReturn($drupal_response); - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld'), ['Content-Type' => 'application/ld+json'], - file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld') + 200 ); $this->expectException(\RuntimeException::class, null, 500); @@ -179,10 +180,10 @@ public function testUpdateNodeThrows412OnStaleContent() $this->drupal_client_prophecy->get(Argument::any(), Argument::any()) ->willReturn($drupal_response); - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld'), ['Content-Type' => 'application/ld+json'], - file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld') + 200 ); $milliner = $this->setupMilliner($this->ok_response, $fedora_get_response, null); @@ -207,10 +208,10 @@ public function testUpdateNodeThrows412OnStaleContent() */ public function testUpdateNodeReturnsFedora201() { - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld'), ['Content-Type' => 'application/ld+json'], - file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld') + 200, ); $milliner = $this->setupMilliner($this->ok_response, $fedora_get_response, $this->created_response); @@ -239,10 +240,10 @@ public function testUpdateNodeReturnsFedora201() public function testUpdateNodeReturnsFedora204() { - $fedora_get_response = new Response( - 200, + $fedora_get_response = new \donatj\MockWebServer\Response( + file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld'), ['Content-Type' => 'application/ld+json'], - file_get_contents(__DIR__ . '/static/ContentLDP-RS.jsonld') + 200 ); $milliner = $this->setupMilliner($this->ok_response, $fedora_get_response, $this->no_content_response); @@ -263,32 +264,38 @@ public function testUpdateNodeReturnsFedora204() /** * Utility function to setup a MillinerService * - * @param Response|null $fedora_head_response + * @param \donatj\MockWebServer\Response|null $fedora_head_response * The response Fedora will return to the HEAD request, if null don't set the prophecy. - * @param Response|null $fedora_get_response + * @param \donatj\MockWebServer\Response|null $fedora_get_response * The response Fedora will return to the GET request, if null don't set the prophecy. - * @param Response|null $fedora_save_response + * @param \donatj\MockWebServer\Response|null $fedora_save_response * The response Fedora will return to the PUT request, if null don't set the prophecy. * - * @return \Islandora\Milliner\Service\MillinerService + * @return \App\Islandora\Milliner\Service\MillinerService */ private function setupMilliner( - $fedora_head_response, - $fedora_get_response, - $fedora_save_response + ?\donatj\MockWebServer\Response $fedora_head_response, + ?\donatj\MockWebServer\Response $fedora_get_response, + ?\donatj\MockWebServer\Response $fedora_save_response ): MillinerService { + $by_method = []; if ($fedora_head_response !== null) { - $this->fedora_client_prophecy->getResourceHeaders(Argument::any()) - ->willReturn($fedora_head_response); + $by_method[ResponseByMethod::METHOD_HEAD] = $fedora_head_response; } if ($fedora_get_response != null) { - $this->fedora_client_prophecy->getResource(Argument::any(), Argument::any(), Argument::any()) - ->willReturn($fedora_get_response); + $by_method[ResponseByMethod::METHOD_GET] = $fedora_get_response; } if ($fedora_save_response !== null) { - $this->fedora_client_prophecy->saveResource(Argument::any(), Argument::any(), Argument::any()) - ->willReturn($fedora_save_response); + $by_method[ResponseByMethod::METHOD_PUT] = $fedora_save_response; + } + if (count($by_method) > 0) { + self::$webserver->setResponseOfPath( + $this->fedora_path, + new ResponseByMethod( + $by_method + ) + ); } return $this->getMilliner(); diff --git a/Recast/.env b/Recast/.env index 344730a0..3d68060c 100644 --- a/Recast/.env +++ b/Recast/.env @@ -9,9 +9,10 @@ # Real environment variables win over .env files. # # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/5.4/configuration/secrets.html # # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration +# https://symfony.com/doc/5.4/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### APP_ENV=dev @@ -19,3 +20,9 @@ APP_SECRET=9ecb305ab0403cacc5ee85a9e480e93e #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=1eee0a12154bda64983764c87ba4c46f +###< lexik/jwt-authentication-bundle ### diff --git a/Recast/README.md b/Recast/README.md index f13ca3e1..0d5a9ef4 100644 --- a/Recast/README.md +++ b/Recast/README.md @@ -59,10 +59,13 @@ environment directory will take precedence over those in the `/path/to/Recast/co The location specified in the configuration file for the log must be writable by the web server. -### Disabling Syn +### Enabling JWT authentication There are instructions in the `/path/to/Recast/config/packages/security.yaml` file describing what to change and what lines -to comment out to disable Syn. +to comment out to enable authentication. + +We use the Lexik JWT Authentication Bundle for Symfony, more information here +https://github.com/lexik/LexikJWTAuthenticationBundle ## Usage diff --git a/Recast/UPGRADE.md b/Recast/UPGRADE.md index bf6f533e..98c98d88 100644 --- a/Recast/UPGRADE.md +++ b/Recast/UPGRADE.md @@ -1,5 +1,38 @@ This document guides you through the process of upgrading Recast. First, check if a section named "Upgrade to x.x.x" exists, with x.x.x being the version you are planning to upgrade to. +## Upgrade to 4.0.0 + +4.0.0 uses Symfony 5.4, which has differences from 3.x.x which used Symfony 4.4. +This makes it non-backwards compatible and requires testing of any custom changes you +may have made. + +Recast relies on Crayfish-Commons `^4.0` which no longer includes our own JWT authentication, +to perform JWT authentication we use the [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle). + +### Upgrade from version 3.x.x + +You can remove the `syn_config:` line from the `./config/packages/crayfish_commons.yaml` file. + +You will need to make a file in `./config/packages` called `lexik_jwt_authentication.yaml`. + +The file needs to contain (at a minimum) or you can copy the file from the Github repository: +```yaml +lexik_jwt_authentication: + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub +``` + +You can either: +* define an [environment variables](https://symfony.com/doc/5.4/configuration.html#configuration-based-on-environment-variables) for + the `JWT_PUBLIC_KEY` variable defined above and pointed to the Drupal public key file + +_or_ +* explicitly overwrite the `'%env(resolve:JWT_PUBLIC_KEY)%'` in + the above file and specify the path to the Drupal public key + + ## Upgrade to 3.0.0 Recast (and all of Crayfish) adheres to [semantic versioning](https://semver.org), which makes a distinction between "major", "minor", and "patch" versions. The upgrade path will be different depending on which previous version from which you are migrating. diff --git a/Recast/composer.json b/Recast/composer.json index a49a78d1..5932624b 100644 --- a/Recast/composer.json +++ b/Recast/composer.json @@ -5,20 +5,25 @@ "require": { "ext-ctype": "*", "ext-iconv": "*", - "islandora/crayfish-commons": "^3.0", - "symfony/dotenv": "4.4.*", + "islandora/crayfish-commons": "^4.0", + "islandora/fedora-entity-mapper": "^1.0", + "lexik/jwt-authentication-bundle": "^2.18", + "symfony/dotenv": "5.4.*", "symfony/flex": "^1.3.1", - "symfony/framework-bundle": "4.4.*", - "symfony/yaml": "4.4.*" + "symfony/framework-bundle": "5.4.*", + "symfony/runtime": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/yaml": "5.4.*" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "sebastian/phpcpd": "^6.0", "squizlabs/php_codesniffer": "^3.0", - "symfony/browser-kit": "4.4.*", - "symfony/css-selector": "4.4.*", - "symfony/phpunit-bridge": "4.4.*" + "symfony/browser-kit": "5.4.*", + "symfony/css-selector": "5.4.*", + "symfony/phpunit-bridge": "5.4.*" }, "minimum-stability": "dev", "prefer-stable": true, @@ -41,7 +46,8 @@ }, "sort-packages": true, "allow-plugins": { - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": true } }, "autoload": { @@ -78,7 +84,7 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "4.4.*" + "require": "5.4.*" } } } diff --git a/Recast/config/bootstrap.php b/Recast/config/bootstrap.php deleted file mode 100644 index 55560fb8..00000000 --- a/Recast/config/bootstrap.php +++ /dev/null @@ -1,23 +0,0 @@ -=1.2) -if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { - (new Dotenv(false))->populate($env); -} else { - // load all the .env files - (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); -} - -$_SERVER += $_ENV; -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/Recast/config/bundles.php b/Recast/config/bundles.php index 17f89aa1..b0a3082c 100644 --- a/Recast/config/bundles.php +++ b/Recast/config/bundles.php @@ -5,4 +5,5 @@ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Islandora\Crayfish\Commons\CrayfishCommonsBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; diff --git a/Recast/config/packages/crayfish_commons.yaml b/Recast/config/packages/crayfish_commons.yaml index 5994e748..da03f30f 100644 --- a/Recast/config/packages/crayfish_commons.yaml +++ b/Recast/config/packages/crayfish_commons.yaml @@ -1,4 +1,3 @@ crayfish_commons: # Because we define a Fedora parameter in the services.yaml we can re-use it here. fedora_base_uri: '%app.fedora_base_url%' - #syn_config: '/path/to/syn-settings.xml' diff --git a/Recast/config/packages/framework.yaml b/Recast/config/packages/framework.yaml index cad7f780..a375c3e7 100644 --- a/Recast/config/packages/framework.yaml +++ b/Recast/config/packages/framework.yaml @@ -1,8 +1,8 @@ -# see https://symfony.com/doc/current/reference/configuration/framework.html +# see https://symfony.com/doc/5.4/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' #csrf_protection: true - #http_method_override: true + http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. @@ -10,8 +10,15 @@ framework: handler_id: null cookie_secure: auto cookie_samesite: lax + storage_factory_id: session.storage.factory.native #esi: true #fragments: true php_errors: log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/Recast/config/packages/lexik_jwt_authentication.yaml b/Recast/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 00000000..c7a868e8 --- /dev/null +++ b/Recast/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,9 @@ +lexik_jwt_authentication: + # Need secret key to generate a token, this is not necessary for normal usage as the key is generated by Drupal. + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + # This is required if you have set a passphrase on the secret key, this is generally not needed. + pass_phrase: '%env(resolve:JWT_PASSPHRASE)%' + # This is the public key from the pair generated by Drupal and is required to validate the JWTs + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + # By default lexik_jwt looks for the username key in the payload, we use sub + user_identity_field: sub diff --git a/Recast/config/packages/security.yaml b/Recast/config/packages/security.yaml index 284b7a30..7dc90667 100644 --- a/Recast/config/packages/security.yaml +++ b/Recast/config/packages/security.yaml @@ -1,9 +1,10 @@ security: - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + enable_authenticator_manager: true + # https://symfony.com/doc/5.4/security.html#where-do-users-come-from-user-providers providers: users_in_memory: { memory: null } - jwt_user_provider: - id: Islandora\Crayfish\Commons\Syn\JwtUserProvider + jwt: + lexik_jwt: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -14,16 +15,14 @@ security: # Need stateless or it reloads the User based on a token. stateless: true - # To enable Syn, uncomment the below 4 lines and change anonymous to false above. - #provider: jwt_user_provider - #guard: - # authenticators: - # - Islandora\Crayfish\Commons\Syn\JwtAuthenticator + # To enable JWT authentication, uncomment the below 2 lines and change anonymous to false above. + #provider: jwt + #jwt: ~ # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#firewalls-authentication + # https://symfony.com/doc/5.4/security.html#firewalls-authentication - # https://symfony.com/doc/current/security/impersonating_user.html + # https://symfony.com/doc/5.4/security/impersonating_user.html # switch_user: true # Easy way to control access for large sections of your site diff --git a/Recast/config/preload.php b/Recast/config/preload.php index 064bdcd6..11d03a58 100644 --- a/Recast/config/preload.php +++ b/Recast/config/preload.php @@ -1,9 +1,6 @@ handle($request); -$response->send(); -$kernel->terminate($request, $response); +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/Recast/src/Controller/RecastController.php b/Recast/src/Controller/RecastController.php index c79a17d8..fa99c3c3 100644 --- a/Recast/src/Controller/RecastController.php +++ b/Recast/src/Controller/RecastController.php @@ -8,7 +8,7 @@ use EasyRdf\RdfNamespace; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; -use Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface; +use Islandora\EntityMapper\EntityMapper; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Request; @@ -21,24 +21,24 @@ class RecastController { /** - * @var \Monolog\Logger + * @var \Psr\Log\LoggerInterface */ - private $log; + private LoggerInterface $log; /** - * @var \Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface + * @var \Islandora\EntityMapper\EntityMapper */ - private $entityMapper; + private EntityMapper $entityMapper; /** * @var \GuzzleHttp\Client */ - private $http; + private Client $http; /** * @var array */ - protected $availableMethods = [ + protected array $availableMethods = [ 'add', 'replace', ]; @@ -47,24 +47,23 @@ class RecastController * The Fedora base url, for URL detection. * @var string */ - private $fcrepo_base_url; + private string $fcrepo_base_url; /** * The Drupal base url, for URL detection. * @var string */ - private $drupal_base_url; + private string $drupal_base_url; /** * Array of Fedora namespace prefixes. * @var array */ - private $namespaces; + private array $namespaces; /** * RecastController constructor. * - * @param \Islandora\Crayfish\Commons\EntityMapper\EntityMapperInterface $entityMapper * @param \GuzzleHttp\Client $http * @param \Psr\Log\LoggerInterface $log * @param string $drupal_base_url @@ -72,14 +71,13 @@ class RecastController * @param array $namespaces */ public function __construct( - EntityMapperInterface $entityMapper, Client $http, LoggerInterface $log, string $drupal_base_url, string $fcrepo_base_url, array $namespaces ) { - $this->entityMapper = $entityMapper; + $this->entityMapper = new EntityMapper(); $this->http = $http; $this->log = $log; $this->drupal_base_url = $drupal_base_url; @@ -313,7 +311,7 @@ private function getFedoraUrl($drupal_url, $fcrepo_base_url, $token) return rtrim($fcrepo_base_url, '/') . '/' . $this->entityMapper->getFedoraPath($uuid); } } catch (RequestException $e) { - $this->log->warn($e->getMessage()); + $this->log->warning($e->getMessage()); return null; } } diff --git a/Recast/src/Kernel.php b/Recast/src/Kernel.php index 6f32cc2f..47e889a1 100644 --- a/Recast/src/Kernel.php +++ b/Recast/src/Kernel.php @@ -3,52 +3,9 @@ namespace App\Islandora\Recast; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Component\Routing\RouteCollectionBuilder; class Kernel extends BaseKernel { use MicroKernelTrait; - - private const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - public function registerBundles(): iterable - { - $contents = require $this->getProjectDir().'/config/bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - - public function getProjectDir(): string - { - return \dirname(__DIR__); - } - - protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void - { - $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); - $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); - } - - protected function configureRoutes(RouteCollectionBuilder $routes): void - { - $confDir = $this->getProjectDir().'/config'; - - $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); - } } diff --git a/Recast/symfony.lock b/Recast/symfony.lock index 7aadeab2..0d7fd4e0 100644 --- a/Recast/symfony.lock +++ b/Recast/symfony.lock @@ -20,6 +20,18 @@ "islandora/crayfish-commons": { "version": "3.0.0" }, + "lexik/jwt-authentication-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, "ml/iri": { "version": "1.1.4" }, @@ -289,18 +301,12 @@ "tests/bootstrap.php" ] }, - "symfony/polyfill-intl-idn": { - "version": "v1.23.0" - }, "symfony/polyfill-intl-normalizer": { "version": "v1.23.0" }, "symfony/polyfill-mbstring": { "version": "v1.23.1" }, - "symfony/polyfill-php72": { - "version": "v1.23.0" - }, "symfony/polyfill-php73": { "version": "v1.23.0" }, @@ -354,6 +360,19 @@ "symfony/service-contracts": { "version": "v2.4.0" }, + "symfony/translation": { + "version": "5.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/var-dumper": { "version": "v4.4.31" }, diff --git a/Recast/tests/RecastControllerTest.php b/Recast/tests/RecastControllerTest.php index 3bac6607..1c6db31d 100644 --- a/Recast/tests/RecastControllerTest.php +++ b/Recast/tests/RecastControllerTest.php @@ -2,16 +2,16 @@ namespace App\Islandora\Recast\Tests; -use Islandora\Crayfish\Commons\EntityMapper\EntityMapper; +use App\Islandora\Recast\Controller\RecastController; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request as GuzzleRequest; use GuzzleHttp\Psr7\Response as GuzzleResponse; -use App\Islandora\Recast\Controller\RecastController; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use Symfony\Component\HttpFoundation\Request; @@ -26,15 +26,15 @@ class RecastControllerTest extends TestCase { use ProphecyTrait; - private $http_prophecy; + private ObjectProphecy $http_prophecy; - private $logger_prophecy; + private ObjectProphecy $logger_prophecy; - private $drupal_base_url = 'localhost:8000'; + private string $drupal_base_url = 'localhost:8000'; - private $fedora_base_url = 'localhost:8080/fcrepo/rest'; + private string $fedora_base_url = 'localhost:8080/fcrepo/rest'; - private $namespaces = [ + private array $namespaces = [ "fedora" => "http://fedora.info/definitions/v4/repository#", "pcdm" => "http://pcdm.org/models#", ]; @@ -189,7 +189,7 @@ public function testPrefixes() /** * Generate a mock response containing mock Fedora body stream. * - * @param string $input_resource + * @param string|null $input_resource * The path to the file containing the stream contents. * @param string $content_type * The content type of the input_resource. @@ -197,8 +197,10 @@ public function testPrefixes() * @return object * The returned stream object. */ - protected function getMockFedoraStream($input_resource = null, $content_type = 'application/ld+json') - { + protected function getMockFedoraStream( + ?string $input_resource = null, + string $content_type = 'application/ld+json' + ): object { if (is_null($input_resource)) { // Provide a default. $input_resource = realpath(__DIR__ . '/resources/drupal_image.json'); @@ -214,11 +216,10 @@ protected function getMockFedoraStream($input_resource = null, $content_type = ' $prophecy = $this->prophesize(ResponseInterface::class); $prophecy->getStatusCode()->willReturn(200); $prophecy->getBody()->willReturn($mock_stream); - $prophecy->getHeader('Content-type')->willReturn($content_type); + $prophecy->getHeader('Content-type')->willReturn([$content_type]); // This is to avoid the describes check, should add a test for it. $prophecy->hasHeader('Link')->willReturn(false); - $mock_fedora_response = $prophecy->reveal(); - return $mock_fedora_response; + return $prophecy->reveal(); } /** @@ -228,7 +229,6 @@ protected function getMockFedoraStream($input_resource = null, $content_type = ' private function getController(): RecastController { return new RecastController( - new EntityMapper(), $this->http_prophecy->reveal(), $this->logger_prophecy->reveal(), $this->drupal_base_url,