From 9504641fb763fd94d71e66ca39cd78144cf72ff1 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 14 Jul 2022 16:34:20 +0200 Subject: [PATCH 01/10] Added module code --- .github/workflows/pr.yaml | 119 ++++++++ .gitignore | 2 + .php-version | 1 + README.md | 144 +++++++++ composer.json | 55 ++++ config/install/rest.resource.entity.file.yml | 21 ++ .../rest.resource.webform_rest_elements.yml | 22 ++ .../rest.resource.webform_rest_fields.yml | 22 ++ .../rest.resource.webform_rest_submission.yml | 22 ++ .../rest.resource.webform_rest_submit.yml | 22 ++ ...add_role_action.os2forms_rest_api_user.yml | 18 ++ ...le_action.os2forms_rest_api_user_write.yml | 18 ++ ...ove_role_action.os2forms_rest_api_user.yml | 18 ++ ...le_action.os2forms_rest_api_user_write.yml | 18 ++ .../user.role.os2forms_rest_api_user.yml | 23 ++ ...user.role.os2forms_rest_api_user_write.yml | 18 ++ os2forms_rest_api.info.yml | 9 + os2forms_rest_api.module | 29 ++ os2forms_rest_api.services.yml | 22 ++ phpcs.xml.dist | 23 ++ src/EventSubscriber/EventSubscriber.php | 105 +++++++ src/WebformHelper.php | 274 ++++++++++++++++++ 22 files changed, 1005 insertions(+) create mode 100644 .github/workflows/pr.yaml create mode 100644 .gitignore create mode 100644 .php-version create mode 100644 composer.json create mode 100644 config/install/rest.resource.entity.file.yml create mode 100644 config/install/rest.resource.webform_rest_elements.yml create mode 100644 config/install/rest.resource.webform_rest_fields.yml create mode 100644 config/install/rest.resource.webform_rest_submission.yml create mode 100644 config/install/rest.resource.webform_rest_submit.yml create mode 100644 config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml create mode 100644 config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml create mode 100644 config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml create mode 100644 config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml create mode 100644 config/install/user.role.os2forms_rest_api_user.yml create mode 100644 config/install/user.role.os2forms_rest_api_user_write.yml create mode 100644 os2forms_rest_api.info.yml create mode 100644 os2forms_rest_api.module create mode 100644 os2forms_rest_api.services.yml create mode 100755 phpcs.xml.dist create mode 100644 src/EventSubscriber/EventSubscriber.php create mode 100644 src/WebformHelper.php diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000..59c34f6 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,119 @@ +on: pull_request +name: PR Review +jobs: + test-composer-files: + name: Validate composer + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: [ '7.4', '8.0', '8.1' ] + dependency-version: [ prefer-lowest, prefer-stable ] + steps: + - uses: actions/checkout@master + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: json + coverage: none + tools: composer:v2 + # https://github.com/shivammathur/setup-php#cache-composer-dependencies + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Validate composer files + run: | + composer validate --strict composer.json + # Check that dependencies resolve. + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + + php-check-coding-standards: + name: PHP - Check Coding Standards + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: [ '7.4', '8.0', '8.1' ] + dependency-version: [ prefer-lowest, prefer-stable ] + steps: + - uses: actions/checkout@master + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: json + coverage: none + tools: composer:v2 + # https://github.com/shivammathur/setup-php#cache-composer-dependencies + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install Dependencies + run: | + composer install --no-interaction --no-progress + - name: PHPCS + run: | + composer coding-standards-check/phpcs + + php-code-analysis: + name: PHP - Code analysis + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: [ '7.4', '8.0', '8.1' ] + dependency-version: [ prefer-lowest, prefer-stable ] + steps: + - uses: actions/checkout@master + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: json, gd + coverage: none + tools: composer:v2 + # https://github.com/shivammathur/setup-php#cache-composer-dependencies + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: drupal-check + run: | + # We need a Drupal project to run drupal-check (cf. https://github.com/mglaman/drupal-check#usage) + # Install Drupal + composer --no-interaction create-project drupal/recommended-project:^9 --stability=dev drupal + # Copy our module source code into the Drupal module folder. + mkdir -p drupal/web/modules/contrib/os2forms_rest_api + cp -r os2forms_rest_api.* composer.json src drupal/web/modules/contrib/os2forms_rest_api + # Add our module as a composer repository. + composer --no-interaction --working-dir=drupal config repositories.os2forms/os2forms_rest_api path web/modules/contrib/os2forms_rest_api + # Restore Drupal composer repository. + composer --no-interaction --working-dir=drupal config repositories.drupal composer https://packages.drupal.org/8 + + # Require our module. + composer --no-interaction --working-dir=drupal require 'os2forms/os2forms_rest_api:*' + + # Check code + composer --no-interaction --working-dir=drupal require --dev drupal/core-dev + cd drupal/web/modules/contrib/os2forms_rest_api + # Remove our non-dev dependencies to prevent duplicated Drupal installation + # PHP Fatal error: Cannot redeclare drupal_get_filename() (previously declared in /home/runner/work/os2forms_rest_api/os2forms_rest_api/drupal/web/modules/contrib/os2forms_rest_api/vendor/drupal/core/includes/bootstrap.inc:190) in /home/runner/work/os2forms_rest_api/os2forms_rest_api/drupal/web/core/includes/bootstrap.inc on line 190 + # Use sed to remove the "require" property in composer.json + sed -i '/^\s*"require":/,/^\s*}/d' composer.json + composer --no-interaction install + composer code-analysis diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/.php-version b/.php-version new file mode 100644 index 0000000..37722eb --- /dev/null +++ b/.php-version @@ -0,0 +1 @@ +7.4 diff --git a/README.md b/README.md index 1edce88..09fc73c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,146 @@ # OS2Forms REST API +We use [Webform REST](https://www.drupal.org/project/webform_rest) to expose a +number of API endpoints. + +## Installation + +```sh +composer require os2forms/os2forms_rest_api +vendor/bin/drush pm:enable os2forms_rest_api +``` + +## Authentication + +We use [Key auth](https://www.drupal.org/project/key_auth) for authenticating +api users. + +A user can access the Webforrm REST API if + +1. it has the “OS2Form REST API user” (`os2forms_rest_api_user`) role and +2. has a generated key (User > Edit > Key authentication; `/user/«user + id»/key-auth`). + +The “OS2Form REST API user” role gives read-only access to the API. To get read +access, a user must also have the “OS2Form REST API user (write)” +(`os2forms_rest_api_user_write`) role. + +## Endpoints + +| Name | Path | Methods | +|--------------------|------------------------------------------------|---------| +| Webform Elements | `/webform_rest/{webform_id}/elements` | GET | +| Webform Fields | `/webform_rest/{webform_id}/fields` | GET | +| Webform Submission | `/webform_rest/{webform_id}/submission/{uuid}` | GET | +| Webform Submit | `/webform_rest/submit` | POST | +| File | `/entity/file/{file_id}` | GET | + +## Examples + +### Get file content from webform submission + +Example uses `some_webform_id` as webform id, `some_submission_id` as submission +id and `dokumenter` as the webform file element key. + +Request: + +```sh +> curl --silent --header 'api-key: …' https://127.0.0.1:8000/webform_rest/some_webform_id/submission/some_submission_uuid +``` + +Response: + +```json +{ + …, + "data": { + "navn": "Jack", + "telefon": "12345678" + "dokumenter": { + "some_document_id", + "some_other_docuent_id" + } + } +} +``` + +Use the file endpoint from above to get information on a file, substituting +`{file_id}` with the actual file id (`some_document_id`) from the previous +request. + +Request: + +```sh +> curl --silent --header 'api-key: …' https://127.0.0.1:8000/webform_rest/entity/file/some_document_id +``` + +Response: + +```json +{ + …, + "uri": [ + { + "value": "private:…", + "url": "/system/files/webform/some_webform_id/…" + } + ], + … +} +``` + +Finally, you can get the actual file by combining the base url +with the url from above response: + +```sh +> curl --silent --header 'api-key: …' http://127.0.0.1:8000/system/files/webform/some_webform_id/… +``` + +Response: + +The actual document content. + +### Submit webform + +Request: + +```sh +> curl --silent --location --header 'api-key: …' --header 'content-type: application/json' https://127.0.0.1:8000/webform_rest/submit --data @- <<'JSON' +{ + "webform_id": "{webform_id}", + "//": "Webform field values (cf. /webform_rest/{webform_id}/fields)", + "navn_": "Mikkel", + "adresse": "Livets landevej", + "mail_": "mikkel@example.com", + "telefonnummer_": "12345678" +} +JSON +``` + +Response: + +```json +{"sid":"6d95afe9-18d1-4a7d-a1bf-fd38c58c7733"} +``` + +(the `sid` value is a webform submission uuid). + +## Custom access control + +To limit access to webforms, you can specify a list of API users that are +allowed to access a webform's data via the API. + +Go to Settings > General > Third party settings > OS2Forms > REST API to specify +which users can access a webform's data. **If no users are specified, all API +users can access the data.** + +### Technical details + +The custom access check is implemented in an event subscriber listening on the +`KernelEvents::REQUEST` event. See +[EventSubscriber::onRequest](src/EventSubscriber/EventSubscriber.php) for +details. + +In order to make documents accessible for api users the Key auth +`authentication_provider` service has been overwritten to be global. See +[os2forms_rest_api.services](os2forms_rest_api.services.yml). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..95ee865 --- /dev/null +++ b/composer.json @@ -0,0 +1,55 @@ +{ + "name": "os2forms/os2forms_rest_api", + "description": "OS2Forms REST API", + "type": "drupal-module", + "license": "MIT", + "authors": [ + { + "name": "Mikkel Ricky", + "email": "rimi@aarhus.dk" + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + ], + "require": { + "drupal/key_auth": "^2.0", + "drupal/webform_rest": "^4.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "drupal/coder": "^8.3", + "mglaman/drupal-check": "^1.4" + }, + "scripts": { + "code-analysis/drupal-check": [ + "vendor/bin/drupal-check --deprecations --analysis --exclude-dir=vendor *.* src" + ], + "code-analysis": [ + "@code-analysis/drupal-check" + ], + "coding-standards-check/phpcs": [ + "vendor/bin/phpcs --standard=phpcs.xml.dist" + ], + "coding-standards-check": [ + "@coding-standards-check/phpcs" + ], + "coding-standards-apply/phpcs": [ + "vendor/bin/phpcbf --standard=phpcs.xml.dist" + ], + "coding-standards-apply": [ + "@coding-standards-apply/phpcs" + ] + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/config/install/rest.resource.entity.file.yml b/config/install/rest.resource.entity.file.yml new file mode 100644 index 0000000..7dccd98 --- /dev/null +++ b/config/install/rest.resource.entity.file.yml @@ -0,0 +1,21 @@ +uuid: ab99862c-61a3-43e2-a7dc-71258f5bdd5c +langcode: en +status: true +dependencies: + module: + - file + - os2forms_rest_api + - serialization + enforced: + module: + - os2forms_rest_api +id: entity.file +plugin_id: 'entity:file' +granularity: resource +configuration: + methods: + - GET + formats: + - json + authentication: + - key_auth diff --git a/config/install/rest.resource.webform_rest_elements.yml b/config/install/rest.resource.webform_rest_elements.yml new file mode 100644 index 0000000..961561c --- /dev/null +++ b/config/install/rest.resource.webform_rest_elements.yml @@ -0,0 +1,22 @@ +uuid: 214c1da5-7fe6-46d9-883e-cd1f0f065e91 +langcode: da +status: true +dependencies: + module: + - key_auth + - os2forms_rest_api + - serialization + - webform_rest + enforced: + module: + - os2forms_rest_api +id: webform_rest_elements +plugin_id: webform_rest_elements +granularity: resource +configuration: + methods: + - GET + formats: + - json + authentication: + - key_auth diff --git a/config/install/rest.resource.webform_rest_fields.yml b/config/install/rest.resource.webform_rest_fields.yml new file mode 100644 index 0000000..b821935 --- /dev/null +++ b/config/install/rest.resource.webform_rest_fields.yml @@ -0,0 +1,22 @@ +uuid: 1d3d3302-95bf-40f3-8704-fd85f7147778 +langcode: da +status: true +dependencies: + module: + - key_auth + - os2forms_rest_api + - serialization + - webform_rest + enforced: + module: + - os2forms_rest_api +id: webform_rest_fields +plugin_id: webform_rest_fields +granularity: resource +configuration: + methods: + - GET + formats: + - json + authentication: + - key_auth diff --git a/config/install/rest.resource.webform_rest_submission.yml b/config/install/rest.resource.webform_rest_submission.yml new file mode 100644 index 0000000..3d18f2a --- /dev/null +++ b/config/install/rest.resource.webform_rest_submission.yml @@ -0,0 +1,22 @@ +uuid: 38a900e0-69d2-4494-ab12-0f71c7f4365b +langcode: da +status: true +dependencies: + module: + - key_auth + - os2forms_rest_api + - serialization + - webform_rest + enforced: + module: + - os2forms_rest_api +id: webform_rest_submission +plugin_id: webform_rest_submission +granularity: resource +configuration: + methods: + - GET + formats: + - json + authentication: + - key_auth diff --git a/config/install/rest.resource.webform_rest_submit.yml b/config/install/rest.resource.webform_rest_submit.yml new file mode 100644 index 0000000..4295bf8 --- /dev/null +++ b/config/install/rest.resource.webform_rest_submit.yml @@ -0,0 +1,22 @@ +uuid: aac6320e-4053-4476-b7f7-763ae8b5a667 +langcode: da +status: true +dependencies: + module: + - key_auth + - os2forms_rest_api + - serialization + - webform_rest + enforced: + module: + - os2forms_rest_api +id: webform_rest_submit +plugin_id: webform_rest_submit +granularity: resource +configuration: + methods: + - POST + formats: + - json + authentication: + - key_auth diff --git a/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml b/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml new file mode 100644 index 0000000..be1b106 --- /dev/null +++ b/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml @@ -0,0 +1,18 @@ +uuid: dd2cd98d-f423-4a2e-8460-d5a5ace4792b +langcode: da +status: true +dependencies: + config: + - user.role.os2forms_rest_api_user + module: + - os2forms_rest_api + - user + enforced: + module: + - os2forms_rest_api +id: user_add_role_action.os2forms_rest_api_user +label: 'Add the OS2Form REST API user role to the selected user(s)' +type: user +plugin: user_add_role_action +configuration: + rid: os2forms_rest_api_user diff --git a/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml b/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml new file mode 100644 index 0000000..e9c26cf --- /dev/null +++ b/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml @@ -0,0 +1,18 @@ +uuid: 4936f0e2-a947-4f18-91a8-5213114ac7b9 +langcode: da +status: true +dependencies: + config: + - user.role.os2forms_rest_api_user_write + module: + - user + - os2forms_rest_api + enforced: + module: + - os2forms_rest_api +id: user_add_role_action.os2forms_rest_api_user_write +label: 'Add the OS2Form REST API user (write) role to the selected user(s)' +type: user +plugin: user_add_role_action +configuration: + rid: os2forms_rest_api_user_write diff --git a/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml new file mode 100644 index 0000000..b64a44b --- /dev/null +++ b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml @@ -0,0 +1,18 @@ +uuid: e015ec9c-0ed8-497e-a3fa-358810c36f33 +langcode: da +status: true +dependencies: + config: + - user.role.os2forms_rest_api_user + module: + - user + - os2forms_rest_api + enforced: + module: + - os2forms_rest_api +id: user_remove_role_action.os2forms_rest_api_user +label: 'Remove the OS2Form REST API user role from the selected user(s)' +type: user +plugin: user_remove_role_action +configuration: + rid: os2forms_rest_api_user diff --git a/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml new file mode 100644 index 0000000..3ca2f4b --- /dev/null +++ b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml @@ -0,0 +1,18 @@ +uuid: c68c0ee2-6a2b-4484-b466-125e7d0bd292 +langcode: da +status: true +dependencies: + config: + - user.role.os2forms_rest_api_user_write + module: + - user + - os2forms_rest_api + enforced: + module: + - os2forms_rest_api +id: user_remove_role_action.os2forms_rest_api_user_write +label: 'Remove the OS2Form REST API user (write) role from the selected user(s)' +type: user +plugin: user_remove_role_action +configuration: + rid: os2forms_rest_api_user_write diff --git a/config/install/user.role.os2forms_rest_api_user.yml b/config/install/user.role.os2forms_rest_api_user.yml new file mode 100644 index 0000000..8d413bc --- /dev/null +++ b/config/install/user.role.os2forms_rest_api_user.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + config: + - rest.resource.webform_rest_elements + - rest.resource.webform_rest_fields + - rest.resource.webform_rest_submission + module: + - key_auth + - os2forms_rest_api + - rest + enforced: + module: + - os2forms_rest_api +id: os2forms_rest_api_user +label: 'OS2Form REST API user' +weight: 1 +is_admin: null +permissions: + - 'restful get webform_rest_elements' + - 'restful get webform_rest_fields' + - 'restful get webform_rest_submission' + - 'use key authentication' diff --git a/config/install/user.role.os2forms_rest_api_user_write.yml b/config/install/user.role.os2forms_rest_api_user_write.yml new file mode 100644 index 0000000..9b79b28 --- /dev/null +++ b/config/install/user.role.os2forms_rest_api_user_write.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - rest.resource.webform_rest_submit + module: + - key_auth + - os2forms_rest_api + - rest + enforced: + module: + - os2forms_rest_api +id: os2forms_rest_api_user_write +label: 'OS2Form REST API (write)' +weight: 2 +is_admin: null +permissions: + - 'restful post webform_rest_submit' diff --git a/os2forms_rest_api.info.yml b/os2forms_rest_api.info.yml new file mode 100644 index 0000000..9c19467 --- /dev/null +++ b/os2forms_rest_api.info.yml @@ -0,0 +1,9 @@ +name: 'OS2Form REST API' +type: module +description: 'OS2Form REST API' +package: Web services +core_version_requirement: ^9 +dependencies: + - drupal:key_auth + - drupal:webform + - drupal:webform_rest diff --git a/os2forms_rest_api.module b/os2forms_rest_api.module new file mode 100644 index 0000000..dced77b --- /dev/null +++ b/os2forms_rest_api.module @@ -0,0 +1,29 @@ + $form + */ +function os2forms_rest_api_webform_third_party_settings_form_alter(array &$form, FormStateInterface $form_state): void { + \Drupal::service(WebformHelper::class)->webformThirdPartySettingsFormAlter($form, $form_state); +} + +/** + * Implements hook_file_download(). + * + * @phpstan-return int|array|null + */ +function os2forms_rest_api_file_download(string $uri) { + return \Drupal::service(WebformHelper::class)->fileDownload($uri); +} diff --git a/os2forms_rest_api.services.yml b/os2forms_rest_api.services.yml new file mode 100644 index 0000000..27d017d --- /dev/null +++ b/os2forms_rest_api.services.yml @@ -0,0 +1,22 @@ +services: + Drupal\os2forms_rest_api\WebformHelper: + arguments: + - '@entity_type.manager' + - '@current_user' + - '@key_auth.authentication.key_auth' + + Drupal\os2forms_rest_api\EventSubscriber\EventSubscriber: + arguments: + - '@current_route_match' + - '@current_user' + - '@Drupal\os2forms_rest_api\WebformHelper' + tags: + - { name: 'event_subscriber' } + + # Overwrite, adding global tag + # @see https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/altering-existing-services-providing-dynamic-services + key_auth.authentication.key_auth: + class: Drupal\key_auth\Authentication\Provider\KeyAuth + arguments: [ '@key_auth' ] + tags: + - { name: authentication_provider, provider_id: 'key_auth', priority: 200, global: true } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100755 index 0000000..0f9161d --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,23 @@ + + + The coding standard. + + . + + vendor/ + + + + + + + + + + + + + + + + diff --git a/src/EventSubscriber/EventSubscriber.php b/src/EventSubscriber/EventSubscriber.php new file mode 100644 index 0000000..aa07124 --- /dev/null +++ b/src/EventSubscriber/EventSubscriber.php @@ -0,0 +1,105 @@ +routeMatch = $routeMatch; + $this->currentUser = $currentUser; + $this->webformHelper = $webformHelper; + } + + /** + * On request handler. + * + * Check for user access to webform API resource. + */ + public function onRequest(KernelEvent $event): void { + $routeName = $this->routeMatch->getRouteName(); + $restRouteNames = [ + 'rest.webform_rest_elements.GET', + 'rest.webform_rest_fields.GET', + 'rest.webform_rest_submission.GET', + 'rest.webform_rest_submission.PATCH', + 'rest.webform_rest_submit.POST', + ]; + if ($this->currentUser->isAnonymous() || !in_array($routeName, $restRouteNames, TRUE)) { + return; + } + + $webformId = $this->routeMatch->getParameter('webform_id'); + $submissionUuid = $this->routeMatch->getParameter('uuid'); + + // Handle webform submission. + if ('rest.webform_rest_submit.POST' === $routeName) { + try { + $content = json_decode($event->getRequest()->getContent(), TRUE, 512, JSON_THROW_ON_ERROR); + $webformId = (string) $content['webform_id']; + } + catch (\JsonException $exception) { + } + } + + if (!isset($webformId)) { + throw new BadRequestHttpException('Cannot get webform id'); + } + + $webform = $this->webformHelper->getWebform($webformId, $submissionUuid); + + if (NULL === $webform) { + return; + } + + $allowedUsers = $this->webformHelper->getAllowedUsers($webform); + if (!empty($allowedUsers) && !isset($allowedUsers[$this->currentUser->id()])) { + throw new AccessDeniedHttpException('Access denied'); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + // @see https://www.drupal.org/project/drupal/issues/2924954#comment-12350447 + KernelEvents::REQUEST => ['onRequest', 31], + ]; + } + +} diff --git a/src/WebformHelper.php b/src/WebformHelper.php new file mode 100644 index 0000000..d6ff161 --- /dev/null +++ b/src/WebformHelper.php @@ -0,0 +1,274 @@ +entityTypeManager = $entityTypeManager; + $this->currentUser = $currentUser; + $this->keyAuth = $keyAuth; + } + + /** + * Implements hook_webform_third_party_settings_form_alter(). + * + * @phpstan-param array $form + */ + public function webformThirdPartySettingsFormAlter(array &$form, FormStateInterface $form_state): void { + /** @var \Drupal\Core\Entity\EntityForm $formObject */ + $formObject = $form_state->getFormObject(); + /** @var \Drupal\webform\WebformInterface $webform */ + $webform = $formObject->getEntity(); + $settings = $webform->getThirdPartySetting('os2forms', 'os2forms_rest_api'); + + $form['third_party_settings']['os2forms']['os2forms_rest_api'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => 'REST API', + '#tree' => TRUE, + ]; + + $allowedUsers = $this->loadUsers($settings['allowed_users'] ?? []); + + $form['third_party_settings']['os2forms']['os2forms_rest_api']['allowed_users'] = [ + '#type' => 'entity_autocomplete', + '#target_type' => 'user', + '#tags' => TRUE, + '#title' => $this->t('Allowed users'), + '#description' => $this->t("Limits users allowed to access this form's data via the REST API"), + '#default_value' => $allowedUsers, + ]; + + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints'] = [ + '#type' => 'fieldset', + '#title' => $this->t('API endpoints'), + + 'links' => [], + + 'messages' => [ + '#markup' => $this->t('Share these endpoints with people that must will use the REST API. Authentification is required to access the endpoints.'), + ], + + ]; + + $routes = [ + 'rest.webform_rest_elements.GET', + 'rest.webform_rest_fields.GET', + 'rest.webform_rest_submission.GET', + ]; + $requireUuid = static function ($route) { + return in_array( + $route, + [ + 'rest.webform_rest_submission.GET', + 'rest.webform_rest_submission.PATCH', + ], + TRUE + ); + }; + + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints']['links']['#prefix'] = '
    '; + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints']['links']['#suffix'] = '
'; + + foreach ($routes as $route) { + $parameters = []; + + if ('rest.webform_rest_submit.POST' !== $route) { + $parameters['webform_id'] = $webform->id(); + } + $uuidPlaceholder = '{uuid}'; + if ($requireUuid($route)) { + $parameters['uuid'] = $uuidPlaceholder; + } + + $url = Url::fromRoute($route, $parameters, ['absolute' => TRUE]); + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints']['links'][$route] = [ + '#type' => 'link', + '#title' => str_replace(urlencode($uuidPlaceholder), $uuidPlaceholder, $url->toString()), + '#url' => $url, + '#prefix' => '
  • ', + '#suffix' => '
  • ', + ]; + } + + if ($this->currentUser->isAuthenticated()) { + /** @var \Drupal\user\Entity\User $apiUser */ + $apiUser = $this->entityTypeManager->getStorage('user')->load($this->currentUser->id()); + // Don't show API data links if current user is not included in + // (non-empty) list of allowed users. + if (!empty($allowedUsers) && !isset($allowedUsers[$apiUser->id()])) { + $apiUser = NULL; + } + $apiKey = $apiUser ? $apiUser->api_key->value : NULL; + if (!empty($apiKey)) { + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints_test'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Test API endpoints'), + + 'links' => [], + + 'message' => [ + '#markup' => $this->t('These are only for checking the API responses for user %user. Do not share these urls!', ['%user' => $apiUser->getAccountName()]), + ], + ]; + + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints_test']['links']['#prefix'] = '
      '; + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints_test']['links']['#suffix'] = '
    '; + + foreach ($routes as $route) { + $parameters = []; + + if ('rest.webform_rest_submit.POST' !== $route) { + $parameters['webform_id'] = $webform->id(); + } + $uuidPlaceholder = '{uuid}'; + if ($requireUuid($route)) { + $parameters['uuid'] = $uuidPlaceholder; + } + $parameters['api-key'] = $apiKey; + + $url = Url::fromRoute($route, $parameters, ['absolute' => TRUE]); + $form['third_party_settings']['os2forms']['os2forms_rest_api']['api_info']['endpoints_test']['links'][$route] = [ + '#type' => 'link', + '#title' => str_replace(urlencode($uuidPlaceholder), $uuidPlaceholder, $url->toString()), + '#url' => $url, + '#prefix' => '
  • ', + '#suffix' => '
  • ', + ]; + } + } + } + } + + /** + * Get webform by id or submission uuid. + * + * If submission uuid is specified (i.e. not null), the submission's webform's + * id must match the specified webform id. + * + * @return \Drupal\webform\WebformInterface|null + * The webform if found. + */ + public function getWebform(string $webformId, string $submissionUuid = NULL): ?WebformInterface { + if (NULL !== $submissionUuid) { + $storage = $this->entityTypeManager->getStorage('webform_submission'); + $submissionIds = $storage + ->getQuery() + ->accessCheck(FALSE) + ->condition('uuid', $submissionUuid) + ->execute(); + $submission = $storage->load(array_key_first($submissionIds)); + + if (NULL === $submission) { + return NULL; + } + + assert($submission instanceof WebformSubmissionInterface); + $webform = $submission->getWebform(); + if ($webformId !== $webform->id()) { + return NULL; + } + + return $webform; + } + + return $this->entityTypeManager + ->getStorage('webform') + ->load($webformId); + } + + /** + * Get users allowed to access a webform's data. + * + * @return \Drupal\user\UserInterface[]|array + * The users. + */ + public function getAllowedUsers(WebformInterface $webform): array { + $settings = $webform->getThirdPartySetting('os2forms', 'os2forms_rest_api'); + $allowedUserIds = $settings['allowed_users'] ?? []; + + return $this->loadUsers($allowedUserIds); + } + + /** + * Load users. + * + * @phpstan-param array $spec + * @phpstan-return array + */ + private function loadUsers(array $spec): array { + return $this->entityTypeManager + ->getStorage('user') + ->loadMultiple(array_column($spec, 'target_id')); + } + + /** + * Implements hook_file_download(). + * + * @phpstan-return int|array|null + */ + public function fileDownload(string $uri) { + $request = \Drupal::request(); + + if ($user = $this->keyAuth->authenticate($request)) { + // Find webform id from uri, see example uri. + // @Example: private://webform/some_webform_id/119/some_file_name.png + $pattern = '/private:\/\/webform\/(?[^\/]*)/'; + if (!preg_match($pattern, $uri, $matches)) { + // Something is not right, deny access. + return -1; + } + + // User has API access. + $webform = \Drupal::entityTypeManager()->getStorage('webform')->load($matches['webform']); + $settings = $webform->getThirdPartySetting('os2forms', 'os2forms_rest_api'); + + $allowedUsers = $this->loadUsers($settings['allowed_users'] ?? []); + + // If allowed users is non-empty and user is not in there deny access. + if (!empty($allowedUsers) && !isset($allowedUsers[$user->id()])) { + return -1; + } + } + return NULL; + } + +} From c633401152cb7fb26223dcf9e98d87b8d102598e Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 22 Aug 2022 14:32:50 +0200 Subject: [PATCH 02/10] Removed uuids from config --- config/install/rest.resource.entity.file.yml | 1 - config/install/rest.resource.webform_rest_elements.yml | 1 - config/install/rest.resource.webform_rest_fields.yml | 1 - config/install/rest.resource.webform_rest_submission.yml | 1 - config/install/rest.resource.webform_rest_submit.yml | 1 - ...system.action.user_add_role_action.os2forms_rest_api_user.yml | 1 - ....action.user_add_role_action.os2forms_rest_api_user_write.yml | 1 - ...tem.action.user_remove_role_action.os2forms_rest_api_user.yml | 1 - ...tion.user_remove_role_action.os2forms_rest_api_user_write.yml | 1 - 9 files changed, 9 deletions(-) diff --git a/config/install/rest.resource.entity.file.yml b/config/install/rest.resource.entity.file.yml index 7dccd98..642cbda 100644 --- a/config/install/rest.resource.entity.file.yml +++ b/config/install/rest.resource.entity.file.yml @@ -1,4 +1,3 @@ -uuid: ab99862c-61a3-43e2-a7dc-71258f5bdd5c langcode: en status: true dependencies: diff --git a/config/install/rest.resource.webform_rest_elements.yml b/config/install/rest.resource.webform_rest_elements.yml index 961561c..3d84b76 100644 --- a/config/install/rest.resource.webform_rest_elements.yml +++ b/config/install/rest.resource.webform_rest_elements.yml @@ -1,4 +1,3 @@ -uuid: 214c1da5-7fe6-46d9-883e-cd1f0f065e91 langcode: da status: true dependencies: diff --git a/config/install/rest.resource.webform_rest_fields.yml b/config/install/rest.resource.webform_rest_fields.yml index b821935..70e581e 100644 --- a/config/install/rest.resource.webform_rest_fields.yml +++ b/config/install/rest.resource.webform_rest_fields.yml @@ -1,4 +1,3 @@ -uuid: 1d3d3302-95bf-40f3-8704-fd85f7147778 langcode: da status: true dependencies: diff --git a/config/install/rest.resource.webform_rest_submission.yml b/config/install/rest.resource.webform_rest_submission.yml index 3d18f2a..8518ea1 100644 --- a/config/install/rest.resource.webform_rest_submission.yml +++ b/config/install/rest.resource.webform_rest_submission.yml @@ -1,4 +1,3 @@ -uuid: 38a900e0-69d2-4494-ab12-0f71c7f4365b langcode: da status: true dependencies: diff --git a/config/install/rest.resource.webform_rest_submit.yml b/config/install/rest.resource.webform_rest_submit.yml index 4295bf8..3fef87f 100644 --- a/config/install/rest.resource.webform_rest_submit.yml +++ b/config/install/rest.resource.webform_rest_submit.yml @@ -1,4 +1,3 @@ -uuid: aac6320e-4053-4476-b7f7-763ae8b5a667 langcode: da status: true dependencies: diff --git a/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml b/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml index be1b106..476efb8 100644 --- a/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml +++ b/config/install/system.action.user_add_role_action.os2forms_rest_api_user.yml @@ -1,4 +1,3 @@ -uuid: dd2cd98d-f423-4a2e-8460-d5a5ace4792b langcode: da status: true dependencies: diff --git a/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml b/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml index e9c26cf..2b23c99 100644 --- a/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml +++ b/config/install/system.action.user_add_role_action.os2forms_rest_api_user_write.yml @@ -1,4 +1,3 @@ -uuid: 4936f0e2-a947-4f18-91a8-5213114ac7b9 langcode: da status: true dependencies: diff --git a/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml index b64a44b..7c5fe60 100644 --- a/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml +++ b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user.yml @@ -1,4 +1,3 @@ -uuid: e015ec9c-0ed8-497e-a3fa-358810c36f33 langcode: da status: true dependencies: diff --git a/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml index 3ca2f4b..4b6b47c 100644 --- a/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml +++ b/config/install/system.action.user_remove_role_action.os2forms_rest_api_user_write.yml @@ -1,4 +1,3 @@ -uuid: c68c0ee2-6a2b-4484-b466-125e7d0bd292 langcode: da status: true dependencies: From e217fd6dc093cb028ab7ca39bb1b41a669aefd01 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 22 Aug 2022 14:36:36 +0200 Subject: [PATCH 03/10] Updated package name and fixed typo --- os2forms_rest_api.info.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/os2forms_rest_api.info.yml b/os2forms_rest_api.info.yml index 9c19467..06a73f5 100644 --- a/os2forms_rest_api.info.yml +++ b/os2forms_rest_api.info.yml @@ -1,7 +1,7 @@ -name: 'OS2Form REST API' +name: 'OS2Forms REST API' type: module -description: 'OS2Form REST API' -package: Web services +description: 'OS2Forms REST API' +package: OS2Forms core_version_requirement: ^9 dependencies: - drupal:key_auth From 480341547ccf8e05816a41c1f283e37abbdfde6d Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 22 Aug 2022 15:01:35 +0200 Subject: [PATCH 04/10] Cleaned up handling of request parameters --- src/EventSubscriber/EventSubscriber.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/EventSubscriber/EventSubscriber.php b/src/EventSubscriber/EventSubscriber.php index aa07124..da3a2b2 100644 --- a/src/EventSubscriber/EventSubscriber.php +++ b/src/EventSubscriber/EventSubscriber.php @@ -63,16 +63,25 @@ public function onRequest(KernelEvent $event): void { return; } - $webformId = $this->routeMatch->getParameter('webform_id'); - $submissionUuid = $this->routeMatch->getParameter('uuid'); - - // Handle webform submission. - if ('rest.webform_rest_submit.POST' === $routeName) { + // GET request have the webform id and (optional) submission uuid in the + // query string. + if (preg_match('/\.GET$/', $routeName)) { + $webformId = $this->routeMatch->getParameter('webform_id'); + $submissionUuid = $this->routeMatch->getParameter('uuid'); + } else { + // POST and PATCH requests have webform id and submission uuid in the + // request body. try { $content = json_decode($event->getRequest()->getContent(), TRUE, 512, JSON_THROW_ON_ERROR); - $webformId = (string) $content['webform_id']; + if (isset($content['webform_id'])) { + $webformId = (string) $content['webform_id']; + } + if (isset($content['submission_uuid'])) { + $submissionUuid = (string) $content['submission_uuid']; + } } catch (\JsonException $exception) { + // Invalid JSON body. We cannot get webform id from request body. } } @@ -80,7 +89,7 @@ public function onRequest(KernelEvent $event): void { throw new BadRequestHttpException('Cannot get webform id'); } - $webform = $this->webformHelper->getWebform($webformId, $submissionUuid); + $webform = $this->webformHelper->getWebform($webformId, $submissionUuid ?? NULL); if (NULL === $webform) { return; From c9c1f1d99989fc95dc3f33eeff08a3ecee40c40e Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 22 Aug 2022 15:02:06 +0200 Subject: [PATCH 05/10] Fixed security issue --- src/EventSubscriber/EventSubscriber.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/EventSubscriber/EventSubscriber.php b/src/EventSubscriber/EventSubscriber.php index da3a2b2..e473c69 100644 --- a/src/EventSubscriber/EventSubscriber.php +++ b/src/EventSubscriber/EventSubscriber.php @@ -59,7 +59,7 @@ public function onRequest(KernelEvent $event): void { 'rest.webform_rest_submission.PATCH', 'rest.webform_rest_submit.POST', ]; - if ($this->currentUser->isAnonymous() || !in_array($routeName, $restRouteNames, TRUE)) { + if (!in_array($routeName, $restRouteNames, TRUE)) { return; } @@ -95,8 +95,11 @@ public function onRequest(KernelEvent $event): void { return; } + // Never allow access for anonymous users. + // If allowed users are set on the form, the current must be in the list to + // get access. $allowedUsers = $this->webformHelper->getAllowedUsers($webform); - if (!empty($allowedUsers) && !isset($allowedUsers[$this->currentUser->id()])) { + if ($this->currentUser->isAnonymous() || (!empty($allowedUsers) && !isset($allowedUsers[$this->currentUser->id()]))) { throw new AccessDeniedHttpException('Access denied'); } } From 695493bb0d099056d7488599dd4c58a790280c9e Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 22 Aug 2022 15:08:52 +0200 Subject: [PATCH 06/10] Applied coding standards --- src/EventSubscriber/EventSubscriber.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EventSubscriber/EventSubscriber.php b/src/EventSubscriber/EventSubscriber.php index e473c69..3532a14 100644 --- a/src/EventSubscriber/EventSubscriber.php +++ b/src/EventSubscriber/EventSubscriber.php @@ -68,7 +68,8 @@ public function onRequest(KernelEvent $event): void { if (preg_match('/\.GET$/', $routeName)) { $webformId = $this->routeMatch->getParameter('webform_id'); $submissionUuid = $this->routeMatch->getParameter('uuid'); - } else { + } + else { // POST and PATCH requests have webform id and submission uuid in the // request body. try { From 902d619d66d35033a4d67520ad40deee819b449a Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 22 Aug 2022 15:26:00 +0200 Subject: [PATCH 07/10] Cleaned up handling of allowed users --- src/WebformHelper.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/WebformHelper.php b/src/WebformHelper.php index d6ff161..1e6fda8 100644 --- a/src/WebformHelper.php +++ b/src/WebformHelper.php @@ -57,7 +57,6 @@ public function webformThirdPartySettingsFormAlter(array &$form, FormStateInterf $formObject = $form_state->getFormObject(); /** @var \Drupal\webform\WebformInterface $webform */ $webform = $formObject->getEntity(); - $settings = $webform->getThirdPartySetting('os2forms', 'os2forms_rest_api'); $form['third_party_settings']['os2forms']['os2forms_rest_api'] = [ '#type' => 'details', @@ -66,8 +65,7 @@ public function webformThirdPartySettingsFormAlter(array &$form, FormStateInterf '#tree' => TRUE, ]; - $allowedUsers = $this->loadUsers($settings['allowed_users'] ?? []); - + $allowedUsers = $this->getAllowedUsers($webform); $form['third_party_settings']['os2forms']['os2forms_rest_api']['allowed_users'] = [ '#type' => 'entity_autocomplete', '#target_type' => 'user', @@ -259,9 +257,7 @@ public function fileDownload(string $uri) { // User has API access. $webform = \Drupal::entityTypeManager()->getStorage('webform')->load($matches['webform']); - $settings = $webform->getThirdPartySetting('os2forms', 'os2forms_rest_api'); - - $allowedUsers = $this->loadUsers($settings['allowed_users'] ?? []); + $allowedUsers = $this->getAllowedUsers($webform); // If allowed users is non-empty and user is not in there deny access. if (!empty($allowedUsers) && !isset($allowedUsers[$user->id()])) { From 1b27d0d640309c4f80993969ab217cb112f85330 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 25 Aug 2022 14:05:56 +0200 Subject: [PATCH 08/10] Added check for missing webform --- src/WebformHelper.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/WebformHelper.php b/src/WebformHelper.php index 1e6fda8..7ad6f0f 100644 --- a/src/WebformHelper.php +++ b/src/WebformHelper.php @@ -255,15 +255,20 @@ public function fileDownload(string $uri) { return -1; } - // User has API access. - $webform = \Drupal::entityTypeManager()->getStorage('webform')->load($matches['webform']); - $allowedUsers = $this->getAllowedUsers($webform); + // User has API access. Try to load the webform. + $webform = $this->getWebform($matches['webform']); + if (NULL === $webform) { + // Deny access if webform cannot be loaded. + return -1; + } + $allowedUsers = $this->getAllowedUsers($webform); // If allowed users is non-empty and user is not in there deny access. if (!empty($allowedUsers) && !isset($allowedUsers[$user->id()])) { return -1; } } + return NULL; } From 4a438e09e94dee7f0e49013e71d1025bc3b11726 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 25 Aug 2022 14:20:21 +0200 Subject: [PATCH 09/10] Added dependency injection --- os2forms_rest_api.services.yml | 1 + src/WebformHelper.php | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/os2forms_rest_api.services.yml b/os2forms_rest_api.services.yml index 27d017d..1d0772e 100644 --- a/os2forms_rest_api.services.yml +++ b/os2forms_rest_api.services.yml @@ -4,6 +4,7 @@ services: - '@entity_type.manager' - '@current_user' - '@key_auth.authentication.key_auth' + - '@request_stack' Drupal\os2forms_rest_api\EventSubscriber\EventSubscriber: arguments: diff --git a/src/WebformHelper.php b/src/WebformHelper.php index 7ad6f0f..ac09ae2 100644 --- a/src/WebformHelper.php +++ b/src/WebformHelper.php @@ -10,6 +10,7 @@ use Drupal\key_auth\Authentication\Provider\KeyAuth; use Drupal\webform\WebformInterface; use Drupal\webform\WebformSubmissionInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * Webform helper for helping with webforms. @@ -38,13 +39,21 @@ class WebformHelper { */ private KeyAuth $keyAuth; + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + private RequestStack $requestStack; + /** * Constructor. */ - public function __construct(EntityTypeManagerInterface $entityTypeManager, AccountProxyInterface $currentUser, KeyAuth $keyAuth) { + public function __construct(EntityTypeManagerInterface $entityTypeManager, AccountProxyInterface $currentUser, KeyAuth $keyAuth, RequestStack $requestStack) { $this->entityTypeManager = $entityTypeManager; $this->currentUser = $currentUser; $this->keyAuth = $keyAuth; + $this->requestStack = $requestStack; } /** @@ -244,7 +253,7 @@ private function loadUsers(array $spec): array { * @phpstan-return int|array|null */ public function fileDownload(string $uri) { - $request = \Drupal::request(); + $request = $this->requestStack->getCurrentRequest(); if ($user = $this->keyAuth->authenticate($request)) { // Find webform id from uri, see example uri. From d8e06b69ee9dc6ecb1ed6432a1544a91222b4ecb Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 29 Aug 2022 10:13:10 +0200 Subject: [PATCH 10/10] Cleaned up code and comments --- src/EventSubscriber/EventSubscriber.php | 6 +-- src/WebformHelper.php | 58 +++++++++++++++++-------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/EventSubscriber/EventSubscriber.php b/src/EventSubscriber/EventSubscriber.php index 3532a14..e67ecdf 100644 --- a/src/EventSubscriber/EventSubscriber.php +++ b/src/EventSubscriber/EventSubscriber.php @@ -96,11 +96,7 @@ public function onRequest(KernelEvent $event): void { return; } - // Never allow access for anonymous users. - // If allowed users are set on the form, the current must be in the list to - // get access. - $allowedUsers = $this->webformHelper->getAllowedUsers($webform); - if ($this->currentUser->isAnonymous() || (!empty($allowedUsers) && !isset($allowedUsers[$this->currentUser->id()]))) { + if (!$this->webformHelper->hasWebformAccess($webform, $this->currentUser)) { throw new AccessDeniedHttpException('Access denied'); } } diff --git a/src/WebformHelper.php b/src/WebformHelper.php index ac09ae2..740534f 100644 --- a/src/WebformHelper.php +++ b/src/WebformHelper.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; @@ -228,13 +229,36 @@ public function getWebform(string $webformId, string $submissionUuid = NULL): ?W * @return \Drupal\user\UserInterface[]|array * The users. */ - public function getAllowedUsers(WebformInterface $webform): array { + private function getAllowedUsers(WebformInterface $webform): array { $settings = $webform->getThirdPartySetting('os2forms', 'os2forms_rest_api'); $allowedUserIds = $settings['allowed_users'] ?? []; return $this->loadUsers($allowedUserIds); } + /** + * Check if a user has access to a webform. + * + * A user has access to a webform if the list of allowed users is empty or the + * user is included in the list. + * + * @param \Drupal\webform\WebformInterface $webform + * The webform. + * @param \Drupal\Core\Session\AccountInterface|int $user + * The user or user id. + * + * @return bool + * True if user has access to the webform. + */ + public function hasWebformAccess(WebformInterface $webform, $user): bool { + $userId = $user instanceof AccountInterface ? $user->id() : $user; + assert(is_int($userId)); + + $allowedUsers = $this->getAllowedUsers($webform); + + return empty($allowedUsers) || isset($allowedUsers[$userId]); + } + /** * Load users. * @@ -250,34 +274,34 @@ private function loadUsers(array $spec): array { /** * Implements hook_file_download(). * + * Note: This is only used to deny access to a file that is attached to a + * webform (submission) that the user does not have permission to access. + * Permission to access private files are handles elsewhere. + * * @phpstan-return int|array|null */ public function fileDownload(string $uri) { $request = $this->requestStack->getCurrentRequest(); + // We are only concerned with users authenticated via Key Auth (cf. + // os2forms_rest_api.services.yml). if ($user = $this->keyAuth->authenticate($request)) { // Find webform id from uri, see example uri. // @Example: private://webform/some_webform_id/119/some_file_name.png $pattern = '/private:\/\/webform\/(?[^\/]*)/'; - if (!preg_match($pattern, $uri, $matches)) { - // Something is not right, deny access. - return -1; - } - - // User has API access. Try to load the webform. - $webform = $this->getWebform($matches['webform']); - if (NULL === $webform) { - // Deny access if webform cannot be loaded. - return -1; - } - - $allowedUsers = $this->getAllowedUsers($webform); - // If allowed users is non-empty and user is not in there deny access. - if (!empty($allowedUsers) && !isset($allowedUsers[$user->id()])) { - return -1; + if (preg_match($pattern, $uri, $matches)) { + $webform = $this->getWebform($matches['webform']); + if (NULL !== $webform) { + // Deny access to file if user does not have access to the webform. + if (!$this->hasWebformAccess($webform, $user)) { + return -1; + } + } } } + // We cannot deny access to the file. Let others handle the access control + // for the (private) file. return NULL; }