diff --git a/composer.json b/composer.json index 405ff8f41..a635b2b3e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "drupal/config_ignore": "^2.1", "drupal/config_split": "^1.4", "drupal/config_update": "^1.6", - "drupal/core": "8.8.x", + "drupal/core": "8.9.x", "drupal/diff": "^1.0-RC2", "drupal/editor_advanced_link": "^1.4", "drupal/entity_browser": "^1.6", @@ -45,6 +45,7 @@ "drupal/redirect": "^1.3", "drupal/role_delegation": "^1.0-alpha1", "drupal/seckit": "^1.1", + "drupal/simple_oauth": "^4.5", "drupal/simple_sitemap": "^2.12", "drupal/smart_trim": "^1.1", "drupal/smtp": "^1.0@RC", @@ -62,7 +63,8 @@ "drupal/link_field_autocomplete_filter": "^1.8", "drupal/queue_mail": "^1.0", "drupal/token": "^1.7", - "drupal/username_enumeration_prevention": "^1.1" + "drupal/username_enumeration_prevention": "^1.1", + "drupal/quick_node_clone": "^1.13" }, "repositories": { "drupal": { @@ -75,7 +77,6 @@ "drupal/core": { "Contextual links should not be added inside another link - https://www.drupal.org/node/2898875": "https://www.drupal.org/files/issues/2018-03-16/contextual_links_should-2898875-6.patch", "Custom blocks are no longer displayed and no longer appear in the custom block library - https://www.drupal.org/project/drupal/issues/2997898#comment-12832813": "https://www.drupal.org/files/issues/2018-10-27/2997898-21.patch", - "Unrecognised entity operation passed to Menu Link Content throws exceptions - https://www.drupal.org/project/drupal/issues/3016038": "https://www.drupal.org/files/issues/2019-03-03/3016038-menu-link-operation-8.patch", "Make the DateTime input format and descriptive text consistent - https://www.drupal.org/project/drupal/issues/2791693#comment-13024583": "https://www.drupal.org/files/issues/2019-03-16/2791693-38.patch", "Content types are ordered by machine name on node/add - https://www.drupal.org/project/drupal/issues/2693485#comment-11004325": "https://www.drupal.org/files/issues/2693485-node-3.patch", "Poor performance when using moderation state views filter - https://www.drupal.org/project/drupal/issues/3097303#comment-13559283": "https://www.drupal.org/files/issues/2020-04-17/3097303-9.patch", @@ -118,6 +119,9 @@ "drupal/scheduled_transitions": { "The revision user is incorrect after CRON ran - https://www.drupal.org/project/scheduled_transitions/issues/3094322": "https://www.drupal.org/files/issues/2019-11-14/3094322_8.x-8.x-1.0-rc3-2.patch" }, + "drupal/simple_oauth": { + "access:false even though permission is checked for the role - https://www.drupal.org/project/simple_oauth/issues/2958159#comment-13847450": "https://www.drupal.org/files/issues/2020-10-05/simple_oauth-refresh_token_authenticated_missing_scope-2958159-4.patch" + }, "drupal/viewsreference": { "Updating from alpha4 to alpha5 via composer results in error - https://www.drupal.org/project/viewsreference/issues/3096956#comment-13370643": "https://www.drupal.org/files/issues/2019-11-28/3096956-17.Unmet-service-dependencies.patch" }, diff --git a/config/install/user.role.site_admin.yml b/config/install/user.role.site_admin.yml index 8f2da20eb..5c18fc69e 100644 --- a/config/install/user.role.site_admin.yml +++ b/config/install/user.role.site_admin.yml @@ -24,6 +24,7 @@ permissions: - 'access webform submission log' - 'administer blocks' - 'administer menu' + - 'administer nodes' - 'administer redirect settings' - 'administer redirects' - 'administer taxonomy' @@ -36,6 +37,7 @@ permissions: - 'assign editor role' - 'assign previewer role' - 'assign site_admin role' + - 'bypass node access' - 'create alert content' - 'create image_gallery content' - 'create landing_page content' diff --git a/includes/helpers.inc b/includes/helpers.inc index 6c1a66447..a2c60e1a0 100644 --- a/includes/helpers.inc +++ b/includes/helpers.inc @@ -9,6 +9,8 @@ use Drupal\config\StorageReplaceDataWrapper; use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\StorageComparer; +use Drupal\Core\Serialization\Yaml; +use Drupal\Core\Site\Settings; /** * Helper to read configuration from provided locations. @@ -152,3 +154,36 @@ function _tide_import_single_config($config_name, array $locations = [], $priori throw $exception; } } + +/** + * Retrieve UUID of a config file in config/sync directory. + * + * @param string $config_name + * The config name. + * + * @return string|null + * The UUID. + */ +function _tide_retrieve_config_uuid(string $config_name) : ?string { + try { + if (version_compare(\Drupal::VERSION, '8.8.0', '<')) { + $config_sync = config_get_config_directory(CONFIG_SYNC_DIRECTORY); + } + else { + $config_sync = Settings::get('config_sync_directory'); + } + if ($config_sync) { + $config_file = $config_sync . DIRECTORY_SEPARATOR . $config_name . '.yml'; + if (file_exists($config_file)) { + $config = Yaml::decode(file_get_contents($config_file)); + if (!empty($config['uuid'])) { + return $config['uuid']; + } + } + } + } + catch (Exception $exception) { + watchdog_exception('tide_core', $exception); + } + return NULL; +} diff --git a/modules/tide_oauth/README.md b/modules/tide_oauth/README.md new file mode 100644 index 000000000..381658647 --- /dev/null +++ b/modules/tide_oauth/README.md @@ -0,0 +1,50 @@ +## Tide OAuth + +### Prerequisites +1. Consumers module: https://www.drupal.org/project/consumers +2. Simple OAuth module: https://www.drupal.org/project/simple_oauth + +### Installation +1. _(Optional)_ Generate a key pair and set environment variables + ```shell script + $ openssl genrsa -out /tmp/private.key 4096 + $ openssl rsa -in /tmp/private.key -pubout > /tmp/public.key + $ export TIDE_OAUTH_PRIVATE_KEY=`cat /tmp/private.key` + $ export TIDE_OAUTH_PUBLIC_KEY=`cat /tmp/public.key` + ``` +2. Enable the `tide_oauth` module. + * If the TIDE_OAUTH_ environment variables are set, the module will copy + the keys to `private://oauth.key` and `private://oauth.pub`. + * Otherwise, the module will generate a new key pair. +3. _(Optional - **Lagoon only**)_ Add a `post-rollout` task to generate the OAuth + key pair from environment variables upon deployment. + ```yaml + tasks: + post-rollout: + - run: + name: Generate OAuth keys from ENV variables. + command: 'drush tide-oauth:keygen' + service: cli + ``` + +### Authentication +1. See the documentation of [Simple OAuth2](https://www.drupal.org/node/2843627) +2. Due to both JWT Authentication module and Simple OAuth module accept + `Authorization: Bearer {TOKEN}` header, Tide OAuth provides extra headers: + * `Authorization: OAuth2 {TOKEN}` + * `X-OAuth2-Authorization: Bearer {TOKEN}` + * `X-OAuth2-Authorization: OAuth2 {TOKEN}` + + When Tide Authenticated Content or JWT module is enabled, all OAuth2 + authentication calls should one of the custom headers as the normal + `Authorization` header is always authenticated against JWT Authentication. +3. By default, the module creates a client `Editorial Preview` with the scope + `editor`. All OAuth2 authentication requests using this client will have + permissions of the `Editor` role. +4. OAuth2 endpoints: + * Authorization URL: `/oauth/authorize` + * Access token URL: `/oauth/token` +5. Default expiration time: + * Access token: 5 minutes + * Authorization code: 5 minutes + * Refresh token: 2 weeks diff --git a/modules/tide_oauth/composer.json b/modules/tide_oauth/composer.json new file mode 100644 index 000000000..c4f8ff4f4 --- /dev/null +++ b/modules/tide_oauth/composer.json @@ -0,0 +1,12 @@ +{ + "name": "dpc-sdp/tide_oauth", + "type": "drupal-drush", + "description": "Drush commands for Tide OAuth.", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + } +} diff --git a/modules/tide_oauth/drush.services.yml b/modules/tide_oauth/drush.services.yml new file mode 100644 index 000000000..73d9e4c48 --- /dev/null +++ b/modules/tide_oauth/drush.services.yml @@ -0,0 +1,6 @@ +services: + tide_oauth.commands: + class: \Drupal\tide_oauth\Commands\TideOauthCommands + arguments: ['@tide_oauth.env_key_generator'] + tags: + - { name: drush.command } diff --git a/modules/tide_oauth/drush/tide_oauth.drush.inc b/modules/tide_oauth/drush/tide_oauth.drush.inc new file mode 100644 index 000000000..8f90ed438 --- /dev/null +++ b/modules/tide_oauth/drush/tide_oauth.drush.inc @@ -0,0 +1,34 @@ + 'Generate OAuth keys from environment variables', + 'drupal dependencies' => ['tide_oauth'], + 'aliases' => ['tokgn', 'tide-oauth:keygen'], + ]; + return $commands; +} + +/** + * Callback for tide-oauth:keygen command. + */ +function drush_tide_oauth_keygen() { + /** @var \Drupal\tide_oauth\EnvKeyGenerator $env_key_generator */ + $env_key_generator = \Drupal::service('tide_oauth.env_key_generator'); + // Generate the OAuth encryption keys from Environment variables. + if ($env_key_generator->generateEnvKeys()) { + // Update Simple OAuth settings. + $env_key_generator->setSimpleOauthKeySettings(); + } + else { + drush_set_error('Could not generate OAuth keys.'); + } +} diff --git a/modules/tide_oauth/src/Authentication/Provider/XSimpleOauthAuthenticationProvider.php b/modules/tide_oauth/src/Authentication/Provider/XSimpleOauthAuthenticationProvider.php new file mode 100644 index 000000000..b3cec7206 --- /dev/null +++ b/modules/tide_oauth/src/Authentication/Provider/XSimpleOauthAuthenticationProvider.php @@ -0,0 +1,56 @@ +headers->get('Authorization', '', TRUE)); + if ((strpos($auth_header, 'OAuth2 ') !== FALSE) || ($auth_header === 'OAuth2')) { + $oauth2_request->headers->add([ + 'Authorization' => str_replace('OAuth2', 'Bearer', $auth_header), + ]); + } + else { + $x_auth_header = trim($oauth2_request->headers->get('X-OAuth2-Authorization', '', TRUE)); + if (($x_auth_header === 'Bearer') || (strpos($x_auth_header, 'Bearer ') !== FALSE)) { + $oauth2_request->headers->add(['Authorization' => $x_auth_header]); + } + elseif (($x_auth_header === 'OAuth2') || (strpos($x_auth_header, 'OAuth2 ') !== FALSE)) { + $oauth2_request->headers->add([ + 'Authorization' => str_replace('OAuth2', 'Bearer', $x_auth_header), + ]); + } + } + + $account = parent::authenticate($oauth2_request); + if ($account) { + // Inherit uploaded files for the current request. + /* @link https://www.drupal.org/project/drupal/issues/2934486 */ + $request->files->add($oauth2_request->files->all()); + // Set consumer ID header on successful authentication, so negotiators + // will trigger correctly. + $request->headers->set('X-Consumer-ID', $account->getConsumer()->uuid()); + } + + return $account; + } + +} diff --git a/modules/tide_oauth/src/Commands/TideOauthCommands.php b/modules/tide_oauth/src/Commands/TideOauthCommands.php new file mode 100644 index 000000000..27d12f390 --- /dev/null +++ b/modules/tide_oauth/src/Commands/TideOauthCommands.php @@ -0,0 +1,54 @@ +envKeyGenerator = $env_key_generator; + } + + /** + * Generate OAuth keys from Environment variables. + * + * @usage drush tide-oauth:keygen + * Generate OAuth keys from Environment variables. + * + * @command tide-oauth:keygen + * @validate-module-enabled tide_oauth + * @aliases tokgn,tide-oauth-keygen + */ + public function generateKeys() { + if ($this->envKeyGenerator->generateEnvKeys()) { + // Update Simple OAuth settings. + $this->envKeyGenerator->setSimpleOauthKeySettings(); + $this->io()->success('OAuth keys have been created.'); + } + else { + $this->io()->error('Could not generate OAuth keys.'); + } + } + +} diff --git a/modules/tide_oauth/src/EnvKeyGenerator.php b/modules/tide_oauth/src/EnvKeyGenerator.php new file mode 100644 index 000000000..c3bca6641 --- /dev/null +++ b/modules/tide_oauth/src/EnvKeyGenerator.php @@ -0,0 +1,177 @@ +fileSystem = $file_system; + $this->fileSystemChecker = $fs_checker; + $this->configFactory = $config_factory; + $this->keyGenerator = $key_generator; + $this->privateKey = getenv(static::ENV_PRIVATE_KEY); + $this->publicKey = getenv(static::ENV_PUBLIC_KEY); + } + + /** + * Check if the keys are set in environment variables. + * + * @return bool + * TRUE if the Environment variables are set. + */ + public function hasEnvKeys() : bool { + return !empty($this->privateKey) && !empty($this->publicKey); + } + + /** + * Generate the OAuth key pairs with Environment variables. + * + * @return bool + * TRUE if the keys are generated. + */ + public function generateEnvKeys() : bool { + if (!$this->hasEnvKeys()) { + return $this->generateOauthKeys(); + } + + $private = 'private://'; + $this->fileSystem->prepareDirectory($private, FileSystemInterface::CREATE_DIRECTORY); + + $key_files = [ + static::FILE_PRIVATE_KEY => $this->privateKey, + static::FILE_PUBLIC_KEY => $this->publicKey, + ]; + foreach ($key_files as $key_uri => $key_content) { + if (@file_exists($key_uri)) { + $this->fileSystem->unlink($key_uri); + } + $this->fileSystemChecker->write($key_uri, $key_content); + $this->fileSystem->chmod($key_uri, 0600); + } + + return TRUE; + } + + /** + * Update Simple OAuth settings with generated keys. + * + * @return bool + * TRUE if succeed. + */ + public function setSimpleOauthKeySettings() : bool { + $real_private_key = $this->fileSystem->realpath(static::FILE_PRIVATE_KEY); + if (!$real_private_key || !@file_exists($real_private_key)) { + return FALSE; + } + $real_public_key = $this->fileSystem->realpath(static::FILE_PUBLIC_KEY); + if (!$real_public_key || !@file_exists($real_public_key)) { + return FALSE; + } + + $settings = $this->configFactory->getEditable('simple_oauth.settings'); + $has_changes = FALSE; + if ($settings->get('private_key') !== $real_private_key) { + $settings->set('private_key', $real_private_key); + $has_changes = TRUE; + } + if ($settings->get('public_key') !== $real_public_key) { + $settings->set('public_key', $real_public_key); + $has_changes = TRUE; + } + if ($has_changes) { + $settings->save(); + } + + return TRUE; + } + + /** + * Generate keys if Environment variables not set. + * + * @return bool + * TRUE if the keys are generated. + */ + public function generateOauthKeys() : bool { + if ($this->hasEnvKeys()) { + return FALSE; + } + + $this->keyGenerator->generateKeys('private://'); + $this->fileSystem->move('private://private.key', static::FILE_PRIVATE_KEY); + $this->fileSystem->chmod(static::FILE_PRIVATE_KEY, 0600); + $this->fileSystem->move('private://public.key', static::FILE_PUBLIC_KEY); + $this->fileSystem->chmod(static::FILE_PUBLIC_KEY, 0600); + + return TRUE; + } + +} diff --git a/modules/tide_oauth/src/PageCache/DisallowXSimpleOauthRequests.php b/modules/tide_oauth/src/PageCache/DisallowXSimpleOauthRequests.php new file mode 100644 index 000000000..eb8b7da69 --- /dev/null +++ b/modules/tide_oauth/src/PageCache/DisallowXSimpleOauthRequests.php @@ -0,0 +1,43 @@ +. + * - Authorization: OAuth2 . + * - X-OAuth2-Authorization: Bearer . + * - X-OAuth2-Authorization: OAuth2 . + * + * @package Drupal\tide_oauth\PageCache + */ +class DisallowXSimpleOauthRequests extends DisallowSimpleOauthRequests { + + /** + * {@inheritdoc} + */ + public function isOauth2Request(Request $request) { + $is_oauth2_requests = parent::isOauth2Request($request); + if ($is_oauth2_requests) { + return TRUE; + } + + // Both JWT and Simple OAuth expects the same 'Authorization: Bearer TOKEN' + // header so we accept the extra 'Authorization: OAuth2 TOKEN'. + $auth_header = trim($request->headers->get('Authorization', '', TRUE)); + if ((strpos($auth_header, 'OAuth2 ') !== FALSE) || ($auth_header === 'OAuth2')) { + return TRUE; + } + // Also accept 'X-OAuth2-Authorization: Bearer TOKEN' + // and 'X-OAuth2-Authorization: OAuth2 TOKEN' headers. + $x_auth_header = trim($request->headers->get('X-OAuth2-Authorization', '', TRUE)); + return (strpos($x_auth_header, 'Bearer ') !== FALSE) || ($x_auth_header === 'Bearer') + || (strpos($x_auth_header, 'OAuth2 ') !== FALSE) || ($x_auth_header === 'OAuth2'); + } + +} diff --git a/modules/tide_oauth/src/Plugin/Field/FieldWidget/MachineNameWidget.php b/modules/tide_oauth/src/Plugin/Field/FieldWidget/MachineNameWidget.php new file mode 100644 index 000000000..26ae5fe7f --- /dev/null +++ b/modules/tide_oauth/src/Plugin/Field/FieldWidget/MachineNameWidget.php @@ -0,0 +1,74 @@ +getSettingsKeys() as $key) { + if ($setting = $this->getSetting($key)) { + $element['value']['#machine_name'][$key] = $setting; + } + } + + // If a label is not provided, use the field label. + if (empty($element['value']['#machine_name']['label'])) { + if ($label = $items->getFieldDefinition()->getLabel()) { + $element['value']['#machine_name']['label'] = (string) $label; + } + } + + return $element; + } + + /** + * Gets the machine name settings. + * + * @return array + * Array of keys. + */ + protected static function getSettingsKeys() { + return [ + 'exists', + 'source', + 'label', + 'replace_pattern', + 'replace', + ]; + } + +} diff --git a/modules/tide_oauth/templates/consumer--with_field_long_description.html.twig b/modules/tide_oauth/templates/consumer--with_field_long_description.html.twig new file mode 100644 index 000000000..d6b9399c1 --- /dev/null +++ b/modules/tide_oauth/templates/consumer--with_field_long_description.html.twig @@ -0,0 +1,41 @@ +{# +/** + * @file + * Default theme implementation to display a node. + * + * Available variables: + * - client: The consumer entity that is requesting access. + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * - content_attributes: Same as attributes, except applied to the main + * content tag that appears in the template. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - view_mode: View mode; for example, "teaser" or "full". + * - is_admin: Flag for admin user status. Will be true when the current user + * is an administrator. + * - logged_in: Flag for logged in users. Will be true when the current user + * is logged in. + * - content: Rendered chidler of the elements array. + * - elements: The raw render array. + * + * @see template_preprocess_consumer() + * + * @ingroup themeable + */ +#} + + +
+ {{ title_prefix }} + {{ label }} + {{ title_suffix }} +
+ +
{{ image }}
+
{{ description }}
+
{{ content.field_long_description }}
+ + diff --git a/modules/tide_oauth/tide_oauth.info.yml b/modules/tide_oauth/tide_oauth.info.yml new file mode 100644 index 000000000..8e4c27b3b --- /dev/null +++ b/modules/tide_oauth/tide_oauth.info.yml @@ -0,0 +1,11 @@ +name: Tide Simple OAuth +description: Provides Simple OAuth for Tide. +type: module +package: Tide +core: 8.x +dependencies: + - consumers:consumers + - simple_oauth:simple_oauth +config_devel: + install: { } + optional: { } diff --git a/modules/tide_oauth/tide_oauth.install b/modules/tide_oauth/tide_oauth.install new file mode 100644 index 000000000..e043b8752 --- /dev/null +++ b/modules/tide_oauth/tide_oauth.install @@ -0,0 +1,324 @@ + $field_name, + 'entity_type' => 'consumer', + 'type' => 'text_long', + 'cardinality' => 1, + 'dependencies' => [ + 'module' => ['consumers', 'text'], + ], + ]); + $uuid = _tide_retrieve_config_uuid('field.storage.consumer.' . $field_name); + if ($uuid) { + $field_storage->set('uuid', $uuid); + } + $field_storage->save(); + } + + $field_config = FieldConfig::loadByName('consumer', 'consumer', $field_name); + if (!$field_config) { + $field_config = FieldConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'consumer', + 'bundle' => 'consumer', + 'label' => 'Long Description', + 'description' => '', + 'settings' => [], + 'field_type' => 'text_long', + 'required' => FALSE, + 'dependencies' => [ + 'config' => ['field.storage.consumer.field_long_description'], + 'module' => ['allowed_formats', 'text'], + ], + 'third_party_settings' => [ + 'allowed_formats' => [ + 'rich_text' => 'rich_text', + 'admin_text' => 'admin_text', + 'plain_text' => 'plain_text', + ], + ], + ]); + $uuid = _tide_retrieve_config_uuid('field.field.consumer.consumer.' . $field_name); + if ($uuid) { + $field_config->setOriginalId($field_config->id()); + $field_config->set('uuid', $uuid); + $field_config->save(); + } + } + + // Update form display and view display. + $field_settings = [ + 'entity_form_display' => [ + $field_name => [ + 'settings' => [ + 'rows' => 5, + 'placeholder' => '', + ], + 'third_party_settings' => [], + 'type' => 'text_textarea', + 'region' => 'content', + 'weight' => 0, + ], + ], + 'entity_view_display' => [ + $field_name => [ + 'weight' => 1, + 'label' => 'hidden', + 'settings' => [], + 'third_party_settings' => [], + 'type' => 'text_default', + 'region' => 'content', + ], + ], + ]; + + foreach ($field_settings as $mode => $fields) { + $entity_display = \Drupal::entityTypeManager() + ->getStorage($mode) + ->load('consumer.consumer.default'); + if (!$entity_display) { + $display_config = [ + 'targetEntityType' => 'consumer', + 'bundle' => 'consumer', + 'mode' => 'default', + 'status' => TRUE, + ]; + switch ($mode) { + case 'entity_form_display': + $entity_display = EntityFormDisplay::create($display_config); + $uuid = _tide_retrieve_config_uuid('core.entity_form_display.consumer.consumer.default'); + if ($uuid) { + $entity_display->set('uuid', $uuid); + } + $entity_display->save(); + break; + + case 'entity_view_display': + $entity_display = EntityViewDisplay::create($display_config); + $uuid = _tide_retrieve_config_uuid('core.entity_view_display.consumer.consumer.default'); + if ($uuid) { + $entity_display->set('uuid', $uuid); + } + $entity_display->save(); + break; + } + } + + if ($entity_display) { + foreach ($fields as $field_name => $field_settings) { + $entity_display->setComponent($field_name, $field_settings)->save(); + } + } + } +} + +/** + * Add the machine_name field to Consumer entity display. + */ +function _tide_oauth_update_consumer_machine_name_field() { + module_load_include('inc', 'tide_core', 'includes/helpers'); + + // Update the UUID of field storage and field config. + $field_name = 'machine_name'; + $field_storage = FieldStorageConfig::loadByName('consumer', $field_name); + if (!$field_storage) { + $field_storage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'consumer', + 'type' => 'string', + 'cardinality' => 1, + 'dependencies' => [ + 'module' => ['consumers'], + ], + 'settings' => [ + 'max_length' => 255, + 'is_ascii' => FALSE, + 'case_sensitive' => FALSE, + ], + ]); + $uuid = _tide_retrieve_config_uuid('field.storage.consumer.' . $field_name); + if ($uuid) { + $field_storage->set('uuid', $uuid); + } + $field_storage->save(); + } + + $field_config = FieldConfig::loadByName('consumer', 'consumer', $field_name); + if (!$field_config) { + $field_config = FieldConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'consumer', + 'bundle' => 'consumer', + 'label' => 'Machine name', + 'description' => '', + 'settings' => [], + 'field_type' => 'string', + 'required' => FALSE, + 'dependencies' => [ + 'config' => ['field.storage.consumer.machine_name'], + ], + ]); + $uuid = _tide_retrieve_config_uuid('field.field.consumer.consumer.' . $field_name); + if ($uuid) { + $field_config->setOriginalId($field_config->id()); + $field_config->set('uuid', $uuid); + $field_config->save(); + } + } + + // Update form display and view display. + $field_settings = [ + 'entity_form_display' => [ + $field_name => [ + 'settings' => [ + 'size' => 60, + 'placeholder' => '', + ], + 'third_party_settings' => [], + 'type' => 'machine_name', + 'region' => 'content', + 'weight' => 0, + ], + ], + 'entity_view_display' => [ + $field_name => [ + 'weight' => 1, + 'label' => 'hidden', + 'settings' => [], + 'third_party_settings' => [], + 'type' => 'string', + 'region' => 'content', + ], + ], + ]; + + foreach ($field_settings as $mode => $fields) { + $entity_display = \Drupal::entityTypeManager() + ->getStorage($mode) + ->load('consumer.consumer.default'); + if (!$entity_display) { + $display_config = [ + 'targetEntityType' => 'consumer', + 'bundle' => 'consumer', + 'mode' => 'default', + 'status' => TRUE, + ]; + switch ($mode) { + case 'entity_form_display': + $entity_display = EntityFormDisplay::create($display_config); + $uuid = _tide_retrieve_config_uuid('core.entity_form_display.consumer.consumer.default'); + if ($uuid) { + $entity_display->set('uuid', $uuid); + } + $entity_display->save(); + break; + + case 'entity_view_display': + $entity_display = EntityViewDisplay::create($display_config); + $uuid = _tide_retrieve_config_uuid('core.entity_view_display.consumer.consumer.default'); + if ($uuid) { + $entity_display->set('uuid', $uuid); + } + $entity_display->save(); + break; + } + } + + if ($entity_display) { + foreach ($fields as $field_name => $field_settings) { + $entity_display->setComponent($field_name, $field_settings)->save(); + } + } + } +} + +/** + * Implements hook_install(). + */ +function tide_oauth_install() { + _tide_oauth_update_consumer_long_description_field(); + _tide_oauth_update_consumer_machine_name_field(); + + // Create the Editorial Preview consumer. + /** @var \Drupal\consumers\Entity\Consumer $consumer */ + $consumer = Consumer::create([ + 'label' => 'Grant access for previews', + 'description' => '', + 'is_default' => FALSE, + 'confidential' => 0, + 'third_party' => 1, + 'roles' => [ + ['target_id' => 'editor'], + ], + 'field_long_description' => [ + [ + 'value' => '

Click the ‘Grant access’ button to allow your CMS user account to connect to the website’s front end to display page previews.

This access is for a set time. You can click and view multiple preview links until your access times out. After your access times out, you’ll have log in again.

', + 'format' => 'rich_text', + ], + ], + 'machine_name' => 'editorial_preview', + ]); + $consumer->save(); + + // Grant the required permissions to default roles. + $roles = [ + 'editor', + 'approver', + 'site_admin', + 'previewer', + ]; + foreach ($roles as $rid) { + /** @var \Drupal\user\RoleInterface $role */ + $role = Role::load($rid); + if ($role) { + $role->grantPermission('grant simple_oauth codes')->save(); + } + } + + /** @var \Drupal\tide_oauth\EnvKeyGenerator $env_key_generator */ + $env_key_generator = \Drupal::service('tide_oauth.env_key_generator'); + /** @var \Drupal\Core\File\FileSystemInterface $fs */ + $fs = \Drupal::service('file_system'); + + // Generate the OAuth encryption keys from Environment variables. + if ($env_key_generator->hasEnvKeys()) { + $env_key_generator->generateEnvKeys(); + } + // Generate new keys. + else { + /** @var \Drupal\simple_oauth\Service\KeyGeneratorService $key_generator */ + $key_generator = \Drupal::service('simple_oauth.key.generator'); + $key_generator->generateKeys('private://'); + $fs->move('private://private.key', EnvKeyGenerator::FILE_PRIVATE_KEY); + $fs->chmod(EnvKeyGenerator::FILE_PRIVATE_KEY, 0600); + $fs->move('private://public.key', EnvKeyGenerator::FILE_PUBLIC_KEY); + $fs->chmod(EnvKeyGenerator::FILE_PUBLIC_KEY, 0600); + } + + // Update Simple OAuth settings. + $env_key_generator->setSimpleOauthKeySettings(); +} diff --git a/modules/tide_oauth/tide_oauth.module b/modules/tide_oauth/tide_oauth.module new file mode 100644 index 000000000..2d1fee6cb --- /dev/null +++ b/modules/tide_oauth/tide_oauth.module @@ -0,0 +1,71 @@ + [ + 'template' => 'consumer--with_field_long_description', + 'base hook' => 'consumer', + ], + ]; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function tide_oauth_theme_suggestions_consumer_alter(array &$suggestions, array $variables) { + /** @var \Drupal\consumers\Entity\Consumer $consumer */ + $consumer = $variables['elements']['#consumer'] ?? NULL; + if (!$consumer) { + return; + } + if ($consumer->hasField('field_long_description')) { + $suggestions[] = 'consumer__with_field_long_description'; + } +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + * + * Hide the Permissions section in Editorial Preview consumer. + * + * @see \Drupal\simple_oauth\Controller\Oauth2AuthorizeForm::buildForm() + */ +function tide_oauth_form_simple_oauth_authorize_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $client_uuid = \Drupal::request()->get('client_id'); + if (!$client_uuid) { + return; + } + /** @var \Drupal\consumers\ConsumerStorage $consumer_storage */ + $consumer_storage = \Drupal::entityTypeManager()->getStorage('consumer'); + /** @var \Drupal\consumers\Entity\Consumer[] $consumers */ + $consumers = $consumer_storage->loadByProperties(['uuid' => $client_uuid]); + if (empty($consumers)) { + return; + } + $consumer = reset($consumers); + if ($consumer->machine_name->value === 'editorial_preview') { + $form['scopes']['#access'] = FALSE; + } + + $form['submit']['#value'] = t('Grant access'); +} diff --git a/modules/tide_oauth/tide_oauth.services.yml b/modules/tide_oauth/tide_oauth.services.yml new file mode 100644 index 000000000..3e82b7e8a --- /dev/null +++ b/modules/tide_oauth/tide_oauth.services.yml @@ -0,0 +1,21 @@ +services: + tide_oauth.authentication.x_simple_oauth: + class: Drupal\tide_oauth\Authentication\Provider\XSimpleOauthAuthenticationProvider + arguments: + - '@simple_oauth.server.resource_server' + - '@entity_type.manager' + - '@tide_oauth.page_cache_request_policy.disallow_x_oauth2_token_requests' + tags: + - { name: authentication_provider, provider_id: tide_oauth2, global: TRUE, priority: 35 } + tide_oauth.page_cache_request_policy.disallow_x_oauth2_token_requests: + class: Drupal\tide_oauth\PageCache\DisallowXSimpleOauthRequests + public: false + tags: + - { name: page_cache_request_policy } + tide_oauth.env_key_generator: + class: Drupal\tide_oauth\EnvKeyGenerator + arguments: + - '@file_system' + - '@simple_oauth.filesystem_checker' + - '@config.factory' + - '@simple_oauth.key.generator' diff --git a/tide_core.info.yml b/tide_core.info.yml index 5577523b5..0632ecb8a 100644 --- a/tide_core.info.yml +++ b/tide_core.info.yml @@ -35,6 +35,7 @@ dependencies: - entity_browser:entity_browser - entity_browser:entity_browser_entity_form - entity_reference_revisions:entity_reference_revisions + - quick_node_clone:quick_node_clone - events_log_track:event_log_track - events_log_track:event_log_track_auth - events_log_track:event_log_track_node diff --git a/tide_core.install b/tide_core.install index f73106efb..561b79ce9 100644 --- a/tide_core.install +++ b/tide_core.install @@ -982,3 +982,34 @@ function tide_core_update_8040() { $permissions = ['administer redirect settings', 'administer redirects']; user_role_revoke_permissions(Role::load($role)->id(), $permissions); } + +/** + * Enables quick node clone module. + */ +function tide_core_update_8041() { + if (!\Drupal::moduleHandler()->moduleExists('quick_node_clone')) { + $module_installer = \Drupal::service('module_installer'); + $module_installer->install(['quick_node_clone']); + } + $roles = [ + 'editor', + 'site_admin', + ]; + $node_types = NodeType::loadMultiple(); + $permissions = []; + foreach ($node_types as $type => $node_type) { + $permissions[] = 'clone ' . $type . ' content'; + } + + foreach ($roles as $role) { + user_role_grant_permissions(Role::load($role)->id(), $permissions); + } +} + +/** + * Updates site_admin permissions. + */ +function tide_core_update_8042() { + $permissions = ['administer nodes', 'bypass node access']; + user_role_grant_permissions(Role::load('site_admin')->id(), $permissions); +}