diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ca1719dc4549c93f204ade6543081b62334ea75c..271a072c7b20126d1ac8b97d076fadcb18df16bf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,4 @@ include: ################ # variables: # OPT_IN_TEST_PREVIOUS_MAJOR: '1' -# SKIP_ESLINT: '1' # OPT_IN_TEST_NEXT_MAJOR: '1' -# _CURL_TEMPLATES_REF: 'main' diff --git a/config/install/services_api_key_auth.settings.yml b/config/install/services_api_key_auth.settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..9ef36be1e11d4b53d2ef6b709533546e2e40e834 --- /dev/null +++ b/config/install/services_api_key_auth.settings.yml @@ -0,0 +1,3 @@ +api_key_request_header_name: 'api_key' +api_key_post_parameter_name: '' +api_key_get_parameter_name: '' diff --git a/config/schema/api_key.schema.yml b/config/schema/api_key.schema.yml deleted file mode 100644 index 7b24a7e432c88b45dd7e130e2234e11dbcb97ad7..0000000000000000000000000000000000000000 --- a/config/schema/api_key.schema.yml +++ /dev/null @@ -1,17 +0,0 @@ -services_api_key_auth.api_key.*: - type: config_entity - label: 'API Key configuration' - mapping: - id: - type: string - label: 'ID' - label: - type: label - label: 'Name' - uuid: - type: string - key: - type: string - Label: 'API Key' - user_uuid: - type: string diff --git a/config/schema/services_api_key_auth.schema.yml b/config/schema/services_api_key_auth.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..83a7efa2e55bc7a6a9e750a45c6d430a1f915371 --- /dev/null +++ b/config/schema/services_api_key_auth.schema.yml @@ -0,0 +1,32 @@ +# Schema for the configuration files of the Request API Key Authentication module. +services_api_key_auth.api_key.*: + type: config_entity + label: 'API Key configuration' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Name' + uuid: + type: string + key: + type: string + Label: 'API Key' + user_uuid: + type: string + +services_api_key_auth.settings: + type: config_object + label: 'Request API Key Authentication settings' + mapping: + api_key_request_header_name: + type: string + label: 'API Key Request header name' + api_key_post_parameter_name: + type: string + label: 'API Key POST parameter name' + api_key_get_parameter_name: + type: string + label: 'API Key GET parameter name' diff --git a/services_api_key_auth.install b/services_api_key_auth.install index 4c709f68e12471991d1c4597981f4559c6e21cdd..5b75c0bf896d33d50f1f5fa4d99115d2fc7ab405 100644 --- a/services_api_key_auth.install +++ b/services_api_key_auth.install @@ -2,14 +2,24 @@ /** * @file + * Install, update and uninstall functions for the services_api_key_auth module. */ declare(strict_types=1); /** - * @file - * Install, update and uninstall functions for the services_api_key_auth module. + * Introduces new settings. + * + * Makes the request api key names configurable. And set their defaults to the + * old programmatically set values. */ +function services_api_key_auth_update_10001() { + \Drupal::configFactory()->getEditable('services_api_key_auth.settings') + ->set('api_key_request_header_name', 'apikey') + ->set('api_key_post_parameter_name', 'api_key') + ->set('api_key_get_parameter_name', 'api_key') + ->save(); +} /** * Service deprecation. @@ -17,6 +27,6 @@ declare(strict_types=1); * The "authentication.api_key_auth" can now be accessed under the name * "services_api_key_auth.authentication.api_key_auth". */ -function services_api_key_auth_update_10001() { +function services_api_key_auth_update_10002() { \Drupal::service('kernel')->rebuildContainer(); } diff --git a/services_api_key_auth.links.menu.yml b/services_api_key_auth.links.menu.yml index 865a30fed1f62045d465297877ff63c81630d42d..498b1a7d44e5f9dd9bc8ad0df4245effed9eb02c 100644 --- a/services_api_key_auth.links.menu.yml +++ b/services_api_key_auth.links.menu.yml @@ -5,3 +5,10 @@ entity.api_key.collection: description: 'Configure API Key registration for users' parent: system.admin_config_services weight: 99 + +services_api_key_auth.api_key_auth_settings: + title: Request API Key Authentication settings + description: The request api key authentication settings + parent: system.admin_config_system + route_name: system.admin_config_services + weight: 10 diff --git a/services_api_key_auth.routing.yml b/services_api_key_auth.routing.yml index dc3d4d75be0f2893a629ad14b2af9e8a30f4a280..7e8364ea83871214e43c2a794deaad18c0df1176 100644 --- a/services_api_key_auth.routing.yml +++ b/services_api_key_auth.routing.yml @@ -32,3 +32,11 @@ entity.api_key.delete_form: parameters: api_key: with_config_overrides: TRUE + +services_api_key_auth.api_key_auth_settings: + path: '/admin/config/services/api-key-auth-settings' + defaults: + _title: 'Request API Key Authentication settings' + _form: 'Drupal\services_api_key_auth\Form\ApiKeyAuthSettingsForm' + requirements: + _permission: 'administer site configuration' diff --git a/src/Authentication/Provider/ApiKeyAuth.php b/src/Authentication/Provider/ApiKeyAuth.php index 2a27cdc0a752a772699fbd94081cad6f25f6e804..c2d5d93af294651e6b40690cbed877201c010d60 100644 --- a/src/Authentication/Provider/ApiKeyAuth.php +++ b/src/Authentication/Provider/ApiKeyAuth.php @@ -126,25 +126,39 @@ class ApiKeyAuth implements AuthenticationProviderInterface { * True if api key is present */ public function getKey(Request $request) { - // Exempt edit/delete form route. + // Exempt this module's api key entitiy edit/delete form route: $route_name = $this->currentRouteMatch->getRouteName(); if (str_contains($route_name ?? '', 'entity.api_key')) { return FALSE; } - $form_api_key = $request->request->get('api_key'); - if (!empty($form_api_key)) { - return $form_api_key; + $settings = $this->configFactory->get('services_api_key_auth.settings'); + + // Check for the api key inside the request header ($_SERVER), if a server + // api key name is defined: + if ($serverApiKeyName = $settings->get('api_key_request_header_name')) { + $header_api_key = $request->headers->get($serverApiKeyName); + if (!empty($header_api_key)) { + return $header_api_key; + } } - $query_api_key = $request->query->get('api_key'); - if (!empty($query_api_key)) { - return $query_api_key; + // Check for the api key inside the request body parameters ($_POST), if a + // post api key name is defined: + if ($postApiKeyName = $settings->get('api_key_post_parameter_name')) { + $form_api_key = $request->request->get($postApiKeyName); + if (!empty($form_api_key)) { + return $form_api_key; + } } - $header_api_key = $request->headers->get('apikey'); - if (!empty($header_api_key)) { - return $header_api_key; + // Check for the api key inside the request query parameters ($_GET), if a + // query api key name is defined: + if ($queryApiKeyName = $settings->get('api_key_get_parameter_name')) { + $query_api_key = $request->query->get($queryApiKeyName); + if (!empty($query_api_key)) { + return $query_api_key; + } } return FALSE; } diff --git a/src/Form/ApiKeyAuthSettingsForm.php b/src/Form/ApiKeyAuthSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..ce79168f9744ec413b7dd3a9e0cc199bc2dc7466 --- /dev/null +++ b/src/Form/ApiKeyAuthSettingsForm.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\services_api_key_auth\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Configure Request API Key Authentication settings for this site. + */ +final class ApiKeyAuthSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'services_api_key_auth_api_key_auth_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return ['services_api_key_auth.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('services_api_key_auth.settings'); + + $form['api_key_request_header_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key Request header name'), + '#default_value' => $config->get('api_key_request_header_name'), + '#description' => $this->t('The name of the API key in the request header parameters ($_SERVER). Leave empty, to disable server API key authentication. NOTE, that the value of this key will be checked on EVERY request for this site.'), + ]; + + $form['api_key_post_parameter_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key POST parameter name'), + '#default_value' => $config->get('api_key_post_parameter_name'), + '#description' => $this->t('The name of the API key in the request body parameters ($_POST). Leave empty, to disable request post API key authentication. NOTE, that the value of this key will be checked on EVERY request for this site.'), + ]; + + $form['api_key_get_parameter_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key GET parameter name'), + '#default_value' => $config->get('api_key_get_parameter_name'), + '#description' => $this->t('The name of the API key in the request query parameters ($_GET). Leave empty, to disable request query API key authentication. NOTE, that the value of this key will be checked on EVERY request for this site.'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('services_api_key_auth.settings') + ->set('api_key_request_header_name', $form_state->getValue('api_key_request_header_name')) + ->set('api_key_post_parameter_name', $form_state->getValue('api_key_post_parameter_name')) + ->set('api_key_get_parameter_name', $form_state->getValue('api_key_get_parameter_name')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/tests/modules/saka_form_test/config/schema/saka_form_test.schema.yml b/tests/modules/saka_form_test/config/schema/saka_form_test.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..db99b517cbb4a3cbe5e1c092db792d4ffcb1ac71 --- /dev/null +++ b/tests/modules/saka_form_test/config/schema/saka_form_test.schema.yml @@ -0,0 +1,8 @@ +# Schema for the configuration files of the Request API Key Authentication Form Test module. +saka_form_test.settings: + type: config_object + label: 'Request API Key Authentication Form Test settings' + mapping: + example: + type: string + label: 'Example' diff --git a/tests/modules/saka_form_test/saka_form_test.info.yml b/tests/modules/saka_form_test/saka_form_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..53a8467e01a3373a7b618b90f67a7cb96b65e8de --- /dev/null +++ b/tests/modules/saka_form_test/saka_form_test.info.yml @@ -0,0 +1,7 @@ +name: 'Request API Key Authentication Form Test' +description: 'A test module which tests, whether "api_key" is a valid form element name.' +type: module +package: Custom +core_version_requirement: ^10.3 || ^11 +dependencies: + - services_api_key_auth:services_api_key_auth diff --git a/tests/modules/saka_form_test/saka_form_test.routing.yml b/tests/modules/saka_form_test/saka_form_test.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..bfdd583b02147c2ce0072ca1eff1e86b180767d9 --- /dev/null +++ b/tests/modules/saka_form_test/saka_form_test.routing.yml @@ -0,0 +1,7 @@ +saka_form_test.test_settings: + path: '/admin/config/system/test-settings' + defaults: + _title: 'Test settings' + _form: 'Drupal\saka_form_test\Form\TestSettingsForm' + requirements: + _permission: 'administer site configuration' diff --git a/tests/modules/saka_form_test/src/Form/TestSettingsForm.php b/tests/modules/saka_form_test/src/Form/TestSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..c57d6b73127c472b5f5f1338ac7c11f028136f8b --- /dev/null +++ b/tests/modules/saka_form_test/src/Form/TestSettingsForm.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\saka_form_test\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Configure Request API Key Authentication Form Test settings for this site. + */ +final class TestSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'saka_form_test_test_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return ['saka_form_test.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['api_key'] = [ + '#type' => 'textfield', + '#title' => $this->t('Api Key'), + '#description' => $this->t('This form element should be submittable without any problem.'), + '#required' => TRUE, + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + $this->messenger()->addMessage($this->t('Form submitted successfully.')); + } + +} diff --git a/tests/src/Functional/ApiKeyFormTest.php b/tests/src/Functional/ApiKeyFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..94207aa72e516e78ecec0d7ed27e01f9e83162a0 --- /dev/null +++ b/tests/src/Functional/ApiKeyFormTest.php @@ -0,0 +1,72 @@ +<?php + +namespace Drupal\Tests\services_api_key_auth\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * This class provides methods specifically for testing something. + * + * @group services_api_key_auth + */ +class ApiKeyFormTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'services_api_key_auth', + 'saka_form_test', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + // Login as root user: + $this->drupalLogin($this->rootUser); + } + + /** + * Tests if the module installation, won't break the site. + * + * @see https://www.drupal.org/project/services_api_key_auth/issues/2928649 + */ + public function testApiKeyFormElementCausingAccessDenied() { + // Set the api_key_post_parameter_name to "api_key": + $this->config('services_api_key_auth.settings') + ->set('api_key_post_parameter_name', 'api_key') + ->save(); + $session = $this->assertSession(); + $page = $this->getSession()->getPage(); + // Go to the test module settings page: + $this->drupalGet('/admin/config/system/test-settings'); + $session->statusCodeEquals(200); + $page->fillField('api_key', 'test-api-key'); + $page->pressButton('Save configuration'); + // Since "api_key" exists as form element and has a value, it will be one + // of the post parameters and therefore collide, with our + // "api_key_post_parameter_name" key name. This will cause an access denied error: + $session->statusCodeEquals(403); + + // Remove the "api_key_post_parameter_name" value, so that authentication through + // post parameters is disabled: + $this->config('services_api_key_auth.settings') + ->set('api_key_post_parameter_name', '') + ->save(); + // Now we should be able to save the form: + $this->drupalGet('/admin/config/system/test-settings'); + $session->statusCodeEquals(200); + $page->fillField('api_key', 'test-api-key'); + $page->pressButton('Save configuration'); + $session->statusCodeEquals(200); + $session->pageTextContains('Form submitted successfully.'); + } + +}