Problem/Motivation

Permissions from contrib modules are migrated "as is" from Drupal 7 even if there is no Drupal 9 module providing the given permission. These permissions appear in the yaml file exports for each role, but do not appear in the UI, and there's no way to clean these up except by scanning through the file to manually remove them from each role, then run configuration import to delete from the database.

Steps to reproduce (Drupal 9):

1) Drupal 7 site contains backup_migrate module.
2) Drupal 7 contains a custom "developer" role (aside from standard Admin) role.
2) Drupal 7 developer role has permissions to use backup and migrate.
3) Do not install Backup Migrate on Drupal 9.
4) Run migrations, export config with drush.

Expected behavior:

See no mention of backup-related permissions in the exported user.role.developer.yml file.

What happened instead?

Yaml file contains lines related to Drupal 7 backup & migrate permissions.

- 'access backup and migrate'
- 'access backup files'
- 'perform backup'

Steps to reproduce (Drupal 10):

See Comments #133, #135. Use the custom migration test_user_role.

  1. Install Drupal and enable the Migrate module: drush si standard and drush en migrate.
  2. Run the migration: drush mim test_user_role.

These steps lead to the following error message:

[error] Adding non-existent permissions to a role is not allowed. The incorrect permissions are "delete any forum content", "foo test permission". (/var/www/html/core/modules/user/src/Entity/Role.php:206)

Proposed resolution

Remove the skip_missing_permission_deprecation flag that was added to the Role entity in #2571235: [regression] Roles should depend on objects that are building the granted permissions and used in the core role migrations.

Add a new UserRole destination plugin that identifies and drops any non-existing permissions. Any role that has non-existing permissions will have a message in the migrate_message table and in the Drupal log that lists all the dropped permissions.

Add optional dependencies to the core role migrations so that permissions that depend on migrated configuration will be valid when the role migration is run.

Remaining tasks

Completed tasks

  • Update the change record. In particular, show how a contrib module can use hook_migration_plugins_alter() to add dependencies to the core role migrations.
  • Review the release-notes snippet.
  • Review the change record.

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

The entity:user_role migrate destination plugin checks for invalid permissions in a migrated role. If there are any, they are removed and they are listed in a log message and a migrate message so that a site administrator can review them later.

CommentFileSizeAuthor
#235 interdiff-2953111-207-231.txt7.01 KBbenjifisher
#231 2953111-231.patch30.4 KBquietone
#231 interdiff-227-231.txt647 bytesquietone
#227 2953111-227.patch30.4 KBquietone
#227 interdiff-223-227.txt1.94 KBquietone
#227 2953111-227-9.5.x.patch31.69 KBquietone
#227 interdiff-223-227-9.5.x.txt1.71 KBquietone
#224 interdiff-2953111-216-223.txt2.09 KBbenjifisher
#223 2953111-223.patch30.4 KBquietone
#223 interdiff-216-223.txt5.11 KBquietone
#223 2953111-223-9.5.x.patch31.08 KBquietone
#223 interdiff-219-223-9.5.x.txt4.49 KBquietone
#219 2953111-219.patch26.17 KBquietone
#219 interdiff-216-219.txt2.07 KBquietone
#216 2953111-216.patch30.04 KBquietone
#216 2953111-216-9.5.x.patch24.86 KBquietone
#216 interdiff-213-216.txt3.99 KBquietone
#213 2953111-213.patch25.75 KBquietone
#213 interdiff-207-213.txt2.92 KBquietone
#207 2953111-207.patch25.74 KBquietone
#207 interdiff-202-207.txt3.63 KBquietone
#206 2953111-206.patch25.74 KBquietone
#206 interdiff-202-206.txt1.33 KBquietone
#202 2953111-202.patch25.71 KBquietone
#202 interdiff-199-202.txt4.83 KBquietone
#199 2953111-199.patch25.48 KBquietone
#199 interdiff-194.199.txt18.5 KBquietone
#194 2953111-194.patch17.8 KBquietone
#194 interdiff-191-194.txt2.18 KBquietone
#191 2953111-191.patch17.35 KBquietone
#191 interdiff-190-191.txt1.73 KBquietone
#189 2953111-189.patch17.28 KBquietone
#189 interdiff-188-189.txt31.42 KBquietone
#188 2953111-188.patch27.4 KBquietone
#188 interdiff-176-188.txt28.22 KBquietone
#176 2953111-176.patch42.16 KBquietone
#176 interdiff-174-176.txt1.69 KBquietone
#174 2953111-174.patch42.9 KBquietone
#174 interdiff-172-174.txt5.14 KBquietone
#173 2953111-172.patch42.63 KBquietone
#173 interdiff-168-172.txt1.07 KBquietone
#168 2953111-168.patch42.72 KBquietone
#168 interdiff-164-168.txt3.16 KBquietone
#166 2953111-164.patch42.44 KBquietone
#166 interdiff-164-166.txt663 bytesquietone
#164 2953111-164.patch42.44 KBquietone
#164 interdiff-163-164.txt620 bytesquietone
#163 2953111-163.patch42.45 KBquietone
#163 interdiff-161-163.txt13.5 KBquietone
#161 2953111-161.patch42.94 KBquietone
#161 interdiff-158-161.txt16.68 KBquietone
#158 2953111-158.patch42.24 KBquietone
#158 interdiff-155-158.txt571 bytesquietone
#156 2953111-155.patch41.9 KBquietone
#156 interdiff-153.155.txt10.83 KBquietone
#153 2953111-153.patch46.49 KBquietone
#153 interdiff-151-153.txt4.14 KBquietone
#151 2953111-151.patch42.22 KBquietone
#151 interdiff-148-151.txt14.29 KBquietone
#148 2953111-148.patch30.61 KBquietone
#148 interdiff-146-148.txt4.64 KBquietone
#148 Screenshot_2022-04-15 Status report.png17.41 KBquietone
#146 2953111-146.patch28.61 KBquietone
#146 interdiff-142-146.txt8.85 KBquietone
#142 2953111-142.patch28.16 KBquietone
#142 interdiff-137-142.txt7.59 KBquietone
#137 2953111-136.patch27.36 KBquietone
#137 interdiff-132-136.txt24.08 KBquietone
#133 test_user_role.yml1.45 KBbenjifisher
#132 interdiff-2953111-130-132.txt732 bytesyogeshmpawar
#132 2953111-132.patch26.55 KByogeshmpawar
#130 interdiff-2953111-127-130.txt592 bytesyogeshmpawar
#130 2953111-130.patch26.14 KByogeshmpawar
#127 interdiff-2953111-126-127.txt579 bytesyogeshmpawar
#127 2953111-127.patch25.56 KByogeshmpawar
#126 2953111-126.patch25.79 KBquietone
#126 interdiff-118-126.txt5.9 KBquietone
#118 2953111-118.patch27.07 KBquietone
#118 interdiff-115-118.txt4.57 KBquietone
#115 2953111-115.patch27.99 KBquietone
#115 interdiff-113-115.txt2.46 KBquietone
#113 2953111-113.patch27.9 KBquietone
#113 interdiff-111-113.txt8.23 KBquietone
#111 2953111-111.patch25.7 KBquietone
#111 interdiff-108-111.txt3.62 KBquietone
#111 Screenshot_2022-03-29 Status report Drupal.png13.29 KBquietone
#108 2953111-108.patch26.49 KBquietone
#108 interdiff-106-108.txt3.92 KBquietone
#106 interdiff_101-106.txt2.24 KBravi.shankar
#106 2953111-106.patch27.14 KBravi.shankar
#101 2953111-101.patch27.92 KBquietone
#101 interdiff-99-101.txt1.71 KBquietone
#98 2953111-99.patch27.82 KBquietone
#98 interdiff-96-99.txt5.12 KBquietone
#98 Screenshot_2022-03-24 Module Permissions site_name.png11.07 KBquietone
#96 2953111-96.patch26.54 KBquietone
#96 interdiff-94-96.txt4.24 KBquietone
#94 2953111-94.patch21.94 KBquietone
#94 interdiff-91-94.txt16.98 KBquietone
#91 2953111-91.patch22.63 KBquietone
#91 interdiff-89-91.txt1.21 KBquietone
#89 2953111-89.patch21.05 KBquietone
#35 interdiff-33-35.txt2.94 KBquietone
#89 diff-88-89.txt1.4 KBquietone
#33 2953111-33.patch25.84 KBquietone
#88 2953111-88.patch21.24 KBquietone
#33 interdiff-32-33.txt8.98 KBquietone
#88 interdiff-86-88.txt1.74 KBquietone
#32 2953111-32.patch25.85 KBquietone
#86 2953111-86.patch21.3 KBquietone
#32 interdiff-30-32.txt7.8 KBquietone
#85 2953111-85.patch21.29 KBquietone
#30 2953111-30.patch26.36 KBquietone
#77 2953111-77.patch24.56 KBquietone
#30 interdiff-29-30.txt997 bytesquietone
#55 2953111-55.patch26.85 KBquietone
#29 2953111-29.patch26.36 KBquietone
#55 interdiff-53-55.txt610 bytesquietone
#29 interdiff-27-29.txt16.81 KBquietone
#53 2953111-53.patch26.82 KBquietone
#27 2953111-27.patch15.16 KBquietone
#20 interdiff-15-20.txt3.26 KBquietone
#53 interdiff-48-53.txt2.17 KBquietone
#49 2953111-48.patch28.05 KBquietone
#27 interdiff-26-27.txt587 bytesquietone
#49 interdiff-44-48.txt8.35 KBquietone
#15 interdiff-11-15.txt3.5 KBquietone
#44 2953111-44.patch28.61 KBquietone
#23 2953111-23.patch15.05 KBquietone
#23 interdiff-20-23.txt7.76 KBquietone
#44 interdiff-42-44.txt21.14 KBquietone
#15 2953111-15.patch11.4 KBquietone
#42 2953111-42.patch28.96 KBquietone
#42 interdiff-39-42.txt31.52 KBquietone
#25 2953111-25.patch15.16 KBquietone
#39 2953111-39.patch26.57 KBquietone
#26 2953111-26.patch15.16 KBquietone
#25 interdiff-23-25.txt2.67 KBquietone
#39 interdiff-32-39.txt501 bytesquietone
#20 2953111-20.patch9.44 KBquietone
#37 2953111-37.patch30.64 KBquietone
#26 interdiff-25-26.txt515 bytesquietone
#37 interdiff-35-37.txt1.43 KBquietone
#35 2953111-35.patch29.81 KBquietone
#11 2953111-11.patch7.17 KBquietone
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jwilson3 created an issue. See original summary.

jwilson3’s picture

Issue summary: View changes
jwilson3’s picture

Issue summary: View changes
jwilson3’s picture

Issue summary: View changes

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

quietone’s picture

Issue tags: +migrate-d7-d8

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

quietone’s picture

Status: Active » Needs review
FileSize
7.17 KB

this adds a destination plugin for entity:user_role that will filter out permissions that do not exist on the destination.

quietone’s picture

Title: Better permissions migration for non-existent contrib modules » Only migrate role permissions that exist on the destination
quietone’s picture

Priority: Normal » Critical

Status: Needs review » Needs work

The last submitted patch, 11: 2953111-11.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
3.5 KB
11.4 KB

Due to #3192893: [META] Serialization issues in Migration tests I had to use the debugger to get the exception messages. They all fail when getting all the permissions in the new EntityUserRole destination,

$this->permissionHandler->getPermissions()

The exception messages are:

d6/MigrateBlockContentTranslationTest.php
'Route "entity.filter_format.edit_form" does not exist.'

d7/MigrateBlockContentTranslationTest.php and d6/MigrateTermNodeTranslationTest.php
'You have requested a non-existent service "locale.config_manager".'

The errors are fixed by adding 'locale' to the list of modules installed in the test. I do not understand why a missing filter_format route is fixed by installing locale nor why the permission handler is requesting 'locale.config_manager' when it does not exist. This seems like a bug somewhere.

quietone’s picture

Issue tags: -migrate +migrate-d6-d8

This affects d6 source as well.

quietone’s picture

Issue summary: View changes
quietone’s picture

Issue summary: View changes
andypost’s picture

+++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
@@ -0,0 +1,92 @@
+  /**
+   * Builds a user role entity destination.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\migrate\plugin\MigrationInterface $migration
+   *   The migration.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The storage for this entity type.
+   * @param array $bundles
+   *   The list of bundles this entity type has.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   * @param \Drupal\user\PermissionHandler $permission_handler
+   *   The module handler.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, PermissionHandler $permission_handler) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory);
+    $this->permissionHandler = $permission_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
+    $entity_type_id = static::getEntityTypeId($plugin_id);
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $migration,
+      $container->get('entity_type.manager')->getStorage($entity_type_id),
+      array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id)),
+      $container->get('language_manager'),
+      $container->get('config.factory'),
+      $container->get('user.permissions')
+    );
+  }

it could use BC compatible patterns like https://git.drupalcode.org/project/drupal/-/blob/9.2.x/core/modules/work...

quietone’s picture

OK, changes made for #19.

benjifisher’s picture

Assigned: Unassigned » benjifisher

I am starting to review this. If I forget to un-assign myself later today, feel free to reassign this issue.

benjifisher’s picture

Assigned: benjifisher » Unassigned
Issue summary: View changes
Status: Needs review » Needs work
Issue tags: +Needs change record, +Needs release note

The new destination class is very simple: kudos to @quietone and to everyone who helped design the migration framework! This class defines the entity:user_role destination plugin. Without this patch, that plugin is provided by the deriver from the EntityConfigBase class.

I looked into the user.permissions service, Drupal\user\PermissionHandler, to see how getPermissions() is implemented. At first, I was worried by method names like buildPermissionsYaml(), since I thought that would just find static permissions defined on YAML files. Looking a little closer, I see that permissions callbacks are declared in the YAML files, and those callbacks are invoked. Just to be sure, I tested \Drupal::service('user.permissions')->getPermissions() to make sure that dynamic permissions like view article revisions are included.

So far, so good!

My main concern is dependencies.

For the core migrations, we have to declare a dependency on any migration that (indirectly) creates new permissions. The obvious examples are the d6_node_type and d7_node_type migrations, which create things like the article content type and then the view article revisions permission, for example.

Is an optional dependency good enough? I think so.

We should review all the .permission.yml files in core modules to find other callbacks, in case there are less obvious examples. I hope we are not creating circular dependencies. Are there any config migrations that depend on d6_user_role or d7_user_role?

The other thing we have to do is warn anyone developing a custom migration to add similar dependencies. In practice, this should not be a big problem: after considering dependencies, migrations are normally run in alphabetical order (I think), so the user_role migrations should run near the end of the config migrations. (Do content migrations have some sort of implicit dependence on config migrations, or does that have to be declared explicitly?) But a custom migration might create a content type in a migration without the d6_ or d7_ prefix, and that could cause a problem.

We need at least a change record, and perhaps also a release note.

The change record should also mention that relevant modules should be enabled before running the migrations. For example, if the source site has the backup_migrate module enabled, as described in the issue summary, then until now there is no need to enable it on the destination site before migration. Now there is.

Could we add an option to the destination plugin and, if set, save a message when roles are filtered out?

There are 9 modules that define permission callbacks:

$ grep -A1 callback core/modules/*/*.permissions.yml
core/modules/content_moderation/content_moderation.permissions.yml:permission_callbacks:
core/modules/content_moderation/content_moderation.permissions.yml-  - \Drupal\content_moderation\Permissions::transitionPermissions
--
core/modules/content_translation/content_translation.permissions.yml:permission_callbacks:
core/modules/content_translation/content_translation.permissions.yml-  - \Drupal\content_translation\ContentTranslationPermissions::contentPermissions
--
core/modules/field_ui/field_ui.permissions.yml:permission_callbacks:
core/modules/field_ui/field_ui.permissions.yml-  - Drupal\field_ui\FieldUiPermissions::fieldPermissions
--
core/modules/filter/filter.permissions.yml:permission_callbacks:
core/modules/filter/filter.permissions.yml-  - Drupal\filter\FilterPermissions::permissions
--
core/modules/layout_builder/layout_builder.permissions.yml:permission_callbacks:
core/modules/layout_builder/layout_builder.permissions.yml-  - \Drupal\layout_builder\LayoutBuilderOverridesPermissions::permissions
--
core/modules/media/media.permissions.yml:permission_callbacks:
core/modules/media/media.permissions.yml-  - \Drupal\media\MediaPermissions::mediaTypePermissions
--
core/modules/node/node.permissions.yml:permission_callbacks:
core/modules/node/node.permissions.yml-  - \Drupal\node\NodePermissions::nodeTypePermissions
--
core/modules/rest/rest.permissions.yml:permission_callbacks:
core/modules/rest/rest.permissions.yml-  - Drupal\rest\RestPermissions::permissions
--
core/modules/taxonomy/taxonomy.permissions.yml:permission_callbacks:
core/modules/taxonomy/taxonomy.permissions.yml-  - Drupal\taxonomy\TaxonomyPermissions::permissions

I will have a look at the tests, but I am adding the tag for a change record and setting this issue to NW for the dependencies.

quietone’s picture

Status: Needs work » Needs review
FileSize
7.76 KB
15.05 KB

@benjifisher, thanks for reviewing this critical.

Ran out of steam just as I was about to leave a comment so this is very brief.

Are there any config migrations that depend on d6_user_role or d7_user_role?

Yes, d6_block and d7_block.

Added the logging of a message to the destination plugin and updated the tests. Also started the change record.

Status: Needs review » Needs work

The last submitted patch, 23: 2953111-23.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
2.67 KB
15.16 KB

Looks like it is failing on ordering. This adds the source rid as the index to the array of expected messages. Maybe that will help.

quietone’s picture

Silly mistake.

quietone’s picture

Again? I don't understand how I keep making this mistake. I guess I should not be making a patch and just before or after an America's Cup race.

benjifisher’s picture

Issue summary: View changes

+1 for always logging the permissions that are dropped. My suggestion to add an option for that was over complicated.

It is moderately expensive to calculate the permissions: read a bunch of YAML files from disk, parse them, run 9 callback functions (more or less). Will it save time if we do this once in the constructor/create() method and save the result in a class variable? Plugins are only instantiated once, right?

I did not number the tasks in my previous comment. Instead, I will update the "Remaining tasks" section in the issue description. One item is additional test coverage. I see the test that "blog" permissions are logged, but we also need a test that these permissions will be migrated if the d?_node_type migration is run first.

quietone’s picture

28.
paragraph 2. I thought the same thing but didn't implement it, don't know why anymore. It has been added.
paragraph 3. Added a test that does the node_type migration first.

I have not added the dependency on node_type to the user role migration.

quietone’s picture

Yes, I did it again.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Needs work

@quietone, thanks for the improvements!

The change to EntityUserRole looks good, saving the list of all permissions in the create() method.

There are a lot of changes to the tests. In general, the test coverage is great, but I have some suggestions below.

Back to NW for these changes and for the remaining items listed in the issue summary.

  1. Can you put all the assert*() helper methods together? I do not feel strongly about whether they should go at the top of the file or the bottom. The existing helpers are at the top, so I would put the new ones there, unless you care more than I do.

  2. Some of the new methods need type declarations and/or @param comments and/or the @param comments need type declarations. (But see the next point before fixing all of these.)

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -50,74 +106,24 @@ protected function assertRole($id, array $permissions, $lookupId, MigrateIdMapIn
         * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
         *   The map table plugin.
         */
     -  protected function assertRoles(MigrateIdMapInterface $id_map) {
     +  protected function assertRoles($data, MigrateIdMapInterface $id_map) {
    
     @@ -157,11 +203,103 @@ public function testUserRole() {
     ...
     +  /**
     +   * Helper to test the logged migrate messages.
     +   *
     +   * @param array $expected_messages
     +   *   An array of log messages.
     +   * @param $id_map
     +   *   The migration ID map plugin.
     +   */
     +  public function assertMessages($expected_messages, $id_map) {
    
     +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php
     @@ -95,12 +105,297 @@ public function testUserRole() {
     ...
     +  /**
     +   * Helper to test the logged migrate messages.
     +   *
     +   * @param array $expected_messages
     +   *   An array of log messages.
     +   * @param $id_map
     +   *   The migration ID map plugin.
     +   */
     +  public function assertMessages($expected_messages, $id_map) {
  3. In the D6 test, the assertRoles() method is always called with the first parameter set to $this->expectedRoleData. It would be simpler to let assertRoles() access the class variable directly, so it does not need the new $data parameter.

  4. The D6 test sorts the permissions in assertRole(), so we could use array_merge() or array_push() here to add just the permissions that are added (indirectly) by the d6_node_type migration. That would emphasize that this is testing what I asked for in #28. On the other hand, this would make the tests less explicit, so maybe it is best to leave it as is.

     @@ -157,11 +203,103 @@ public function testUserRole() {
     ...
     +    // Assert the permissions per role.
     +    $id_map = $this->getMigration('d6_user_role')->getIdMap();
     +    $this->expectedRoleData['migrate_test_role_2']['permissions'] = [
     +      // From permission table.
     +      'access content overview',
     +      'administer nodes',
     +      'create forum content',
     +      'delete any forum content',
     +      'delete own forum content',
     +      'edit any forum content',
     +      'edit own forum content',
     +      'use text format php_code',
     +    ];
     +    $this->assertRoles($this->expectedRoleData, $id_map);
  5. Same snippet: either way, adjust the comments. Make the first something like "After the d6_node_type migration, there are additional roles." I think we can remove the second.

  6. The $roles variable does not change between the two definitions, so we could remove the second. If we do that, then we should probably give it a more descriptive name, maybe $duplicate_role_names.

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php
     @@ -95,12 +105,297 @@ public function testUserRole() {
     ...
     +  public function testUserRoleWithNodeType() {
     ...
     +    // Test there are no duplicated roles.
     +    $roles = [
     +      'anonymous1',
     +      'authenticated1',
     +      'administrator1',
     +    ];
     +    $this->assertEmpty(Role::loadMultiple($roles));
     ...
     +    // Test there are no duplicated roles.
     +    $roles = [
     +      'anonymous1',
     +      'authenticated1',
     +      'administrator1',
     +    ];
     +    $this->assertEmpty(Role::loadMultiple($roles));
  7. Same snippet: consider changing the second comment to "Test there are still no …".

P.S. (8):

    // Tests the migration log contains an error message.

It is a warning message, not an error message.

quietone’s picture

Status: Needs work » Needs review
FileSize
7.8 KB
25.85 KB

@benjifished, thanks for the review. I was initially cautious about further changes to the tests but on reading through they are all sensible and stay within scope.

#31.
1. Moved the helpers to the top to minimize the changes.
2. Fixed
3. Fixed
4. OK, leaving as is
5. Fixed
6. Fixed
7, Fixed
8. Fixed

quietone’s picture

Now add an optional dependency on the node type migration.

This requires some changes to the D6 test. So far, this issue has added a test to d6/MigrateUserRole that added the running of d6_node_type, so there are two tests one without running d6_node_type (testUserRole) and one with running d6_node_type (testUserRoleWithNodeType). However, now that the optional dependency has been added it is no longer a simple task to run the test that does not run d6_node_type. That is because \Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase installs 7 modules, one of which is 'node'. That means that the node migrations are discovered and d6_user_role now requires d6_node_type. Because of the recently added test (testUserRoleWithNodeType) has been removed and the existing test (testUserRole) modified to handle the running of d6_node_type. This is all fine, when a dependency is added the migration test is altered.

For the companion d7/MigrateUserRole this is not a problem, the base class does not install modules so it can have both a testUserRole and a testUserRolwWithNodeType.

I should add that I tried a quick solution of uninstalling node in the D6 test and that fails with

1) Drupal\Tests\user\Kernel\Migrate\d6\MigrateUserRoleTest::testUserRole
Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'test.test80182599users_data' doesn't exist: DELETE FROM "test80182599users_data" 
WHERE "module" IN (:db_condition_placeholder_0); Array
(
    [:db_condition_placeholder_0] => node
)

The test could be done by adding a test module that alters d6_user_role but that isn't needed - we know the dependency system works as we can see with the d7 test.

Status: Needs review » Needs work

The last submitted patch, 33: 2953111-33.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
2.94 KB
29.81 KB

Adding the dependency to d6_user_role broke a lot of tests because of the problem mentioned about the base class enabling modules.

Status: Needs review » Needs work

The last submitted patch, 35: 2953111-35.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
1.43 KB
30.64 KB

Missed two tests.

benjifisher’s picture

Status: Needs review » Needs work

@quietone:

While I was taking a walk before dinner, I thought about how we could make this change less disruptive. Perhaps we could disable the check for whether permissions exist unless the migration specifies validate: true.

Now it is after dinner, and I am reading your comments. I think if we made this validation optional, it would make updating the tests much simpler. I think the trouble you ran into is a sign that the code is too tightly coupled, and adding an option might be the right way to loosen things up a bit.

I am also looking at the existing code, and I see that the validate option is defined in EntityConfigBase, so it is not available in EntityUserRole. (If we were dealing with a content-entity destination, then I guess we could be changing the entity type’s validate() method.) But we can add an option. To avoid confusion, I think we should call it validate_permissions instead of just validate.

The disadvantage of this approach is that validation would be disabled by default. If we want to change that, then we can open a follow-up issue to make validate_permissions a required option, or change the default. I think that would be simpler than the current issue, but more disruptive, so we would want to target Drupal 10.

I am sorry I did not think of this earlier. I guess I should take a walk before dinner more regularly.

I will not do a complete review at this point, but here are a few questions and comments on the latest patch.

  1. You added $this->migrateContentTypes() to several tests. I guess that is so that content types are created, which affects the permissions. But it seems as though you added it to a lot of tests. Do they all end up running the user-role migrations and testing the migrated roles? In any event, we should need fewer changes if we add the validate_permissions option.

  2. You also enabled menu_ui in many of the tests. I guess this is required by migrateContentTypes()?

  3. Why make the node-type migration required for D6 and optional for D7?

     +++ b/core/modules/user/migrations/d6_user_role.yml
     @@ -39,4 +39,5 @@ destination:
        plugin: entity:user_role
      migration_dependencies:
        required:
     +    - d6_node_type
          - d6_filter_format
     +++ b/core/modules/user/migrations/d7_user_role.yml
     @@ -37,4 +37,5 @@ destination:
        plugin: entity:user_role
      migration_dependencies:
        optional:
     +    - d7_node_type
          - d7_filter_format
  4. The @param comment is good, but this function still needs an array declaration:

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -44,72 +50,37 @@ protected function assertRole($id, array $permissions, $lookupId, MigrateIdMapIn
     ...
     +  protected function assertRoles($expected_role_data, MigrateIdMapInterface $id_map) {
  5. This function needs two type declarations:

     ...
     +  public function assertMessages($expected_messages, $id_map) {
  6. And this one:

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php
     @@ -32,31 +31,62 @@ protected function setUp(): void {
     ...
     -  protected function assertEntity($id, $label, $original_rid) {
     +  protected function assertEntity($id, $label, $original_rid, $permissions) {
  7. I remember working on an issue to add permissions for each custom block type. So I think we need to add a dependency on block-type migrations. If I have enough time and energy before bed, I will add another comment with a more comprehensive list.

quietone’s picture

Status: Needs work » Needs review
FileSize
501 bytes
26.57 KB

@benjifisher, I agree that walks are great for problem solving.

I started reading the comment and stopped at

Why make the node-type migration required for D6 and optional for D7?

Oh my, that is a serious mistake and that needs to be fixed before anything else. And it proves my understanding of 'optional' above was wrong. It was correctly behaving as 'required' and I just plain missed it.

This patch is based on #32 and adds the optional dependency on the node_type migration.

benjifisher’s picture

Here are all the permissions callbacks declared in core/modules/*/*.permissions.yml:

  1. \Drupal\content_moderation\Permissions::transitionPermissions Load all the workflows defined using the content_moderation module. Get the transitions for each workflow. Create a permission for each transition. Drupal\Workflows\Transition is not an entity, and I find nothing when I search for /transition/i in core/modules/migrations/*.

  2. \Drupal\content_translation\ContentTranslationPermissions::contentPermissions

    // Create a translate permission for each enabled entity type and (optionally)
    // bundle.

    I think that means we need a dependency on any migration that creates an entity type or a bundle. I do not think there are any migrations that create entity types, but there are several that create bundles:

    • block_content_type.yml
    • d6_comment_type.yml
    • d7_comment_type.yml
    • d6_language_types.yml
    • d7_language_types.yml
    • d6_node_type.yml
    • d7_node_type.yml
    • d6_taxonomy_vocabulary.yml
    • d7_taxonomy_vocabulary.yml

    I found most of those by searching for "type", but that missed the vocabulary migrations.

    There are 25 config entities in Drupal core modules. (Search for "@ConfigEntityType" in core/modules/*/src/**.) I searched the migration YAML files for entity:(block|block_content_type|...) and found 75 matches. They are all the destination plugin in the migration file.

    I think that list has a lot of false positives. Not every config entity describes the bundles of some content entity type. If I only count the config entities that include the bundle_of = ... annotation, then there are just 7. These are the destination plugins in 11 migrations:

    • block_content_type.yml
    • d6_comment_type.yml
    • d7_comment_type.yml
    • d6_taxonomy_vocabulary_translation.yml
    • d7_taxonomy_vocabulary_translation.yml
    • contact_category.yml
    • d6_node_type.yml
    • d7_node_type.yml
    • d7_shortcut_set.yml
    • d6_taxonomy_vocabulary.yml
    • d7_taxonomy_vocabulary.yml

    That seems like a lot of work to find the 4 that I missed and rule out the two language_types migrations.

  3. Drupal\field_ui\FieldUiPermissions::fieldPermissions

  4. Drupal\filter\FilterPermissions::permissions

  5. \Drupal\layout_builder\LayoutBuilderOverridesPermissions::permissions

  6. \Drupal\media\MediaPermissions::mediaTypePermissions

  7. \Drupal\node\NodePermissions::nodeTypePermissions

  8. Drupal\rest\RestPermissions::permissions

  9. Drupal\taxonomy\TaxonomyPermissions::permissions

That is all I have time for tonight.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Needs work

We discussed this issue briefly during #3204288: [meeting] Migrate Meeting 2021-03-25. Using optional dependencies seems like the right thing to do. I still think we should consider adding the validate_permissions option as in #38. For now, I am going to finish what I started in #40.

  1. Drupal\field_ui\FieldUiPermissions::fieldPermissions:

     // Create a permission for each fieldable entity to manage
     // the fields and the display.

    As I said, I do not think there are any migrations that create entity types.

  2. Drupal\filter\FilterPermissions::permissions

     // Generate permissions for each text format. Warn the administrator that any
     // of them are potentially unsafe.

    I think we need dependencies on

    • d6_filter_format
    • d7_filter_format
  3. \Drupal\layout_builder\LayoutBuilderOverridesPermissions::permissions: Load all entity_view_display config entities that are configured for per-entity LB settings. Add two permissions for each. The permission names include "layout overrides", so I do not think any Drupal 6/7 sites would have such permissions. I think we can skip this one.

  4. \Drupal\media\MediaPermissions::mediaTypePermissions:

     // Generate media permissions for all media types.

    There are no core migrations using the destination plugin entity:media_type.

  5. \Drupal\node\NodePermissions::nodeTypePermissions: These are the first permissions we thought of.

  6. Drupal\rest\RestPermissions::permissions: For each RestResourceConfigInterface configuration entity, create permissions defined by its resource plugin.

    There are no migrations that use entity:rest_resource_config as the destination plugin, so I think we can skip this one.

  7. Drupal\taxonomy\TaxonomyPermissions::permissions: For each vocabulary, add CRUD permissions for terms in that vocabulary. I think we already handled this under (2) in the previous comment.

Combining (and sorting) the lists from (2) and (4), I think these are the migrations we need to add as dependencies:

  • block_content_type.yml
  • contact_category.yml
  • d6_comment_type.yml
  • d6_filter_format
  • d6_node_type.yml
  • d6_taxonomy_vocabulary.yml
  • d6_taxonomy_vocabulary_translation.yml
  • d7_comment_type.yml
  • d7_filter_format
  • d7_node_type.yml
  • d7_shortcut_set.yml
  • d7_taxonomy_vocabulary.yml
  • d7_taxonomy_vocabulary_translation.yml

Back to NW to add these dependencies.

quietone’s picture

Status: Needs work » Needs review
FileSize
31.52 KB
28.96 KB

Added all the optional dependencies identified by benjisher (thanks!), added the validate_permissions option to the destinations updated the tests. Both the d6 and d7 test now have a new test method testUserRoleWithPermission that runs all the optional and required migrations with validate_permissions set to TRUE. I reworked the test quite a bit with the aim of making it easier to see the changes in permissions. I hope I succeeded.

benjifisher’s picture

Status: Needs review » Needs work

I think this issue is getting close to done! Of course, I still have a few small suggestions.

I need to get some sleep, so I did not yet review the D7 stest.

  1. Now that we calculate the list of permissions in the create() method, the permission handler can be local to that method. We do not need to make it a class property.

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,66 @@
     ...
     +class EntityUserRole extends EntityConfigBase {
     +
     +  /**
     +   * The permission handler.
     +   *
     +   * @var \Drupal\user\PermissionHandler
     +   */
     +  protected $permissionHandler;
  2. I think that $destinationPermissions is a better name for this variable:

     +  /**
     +   * All permissions on the destination site.
     +   *
     +   * @var string[]
     +   */
     +  protected $allPermissions;
  3. You discussed enabling the locale module in #15. Let’s try again to figure out why this makes a difference.

  4. Let’s be consistent:

     +++ b/core/modules/user/migrations/d6_user_role.yml
     @@ -37,6 +37,14 @@ process:
          - plugin: filter_format_permission
      destination:
        plugin: entity:user_role
     +  validate_permissions: false
    
     +++ b/core/modules/user/migrations/d7_user_role.yml
     @@ -37,4 +37,11 @@ destination:
        plugin: entity:user_role
      migration_dependencies:
        optional:

    Since we might want to make validate_permissions true by default, some time in the future, let’s be consistent by setting it (to false) in both migrations. Besides, this makes it a little more discoverable.

  5. Why call the key lookup_up?

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -14,12 +15,62 @@
     ...
     +    foreach ($data as $datum) {
     +      $this->sourcePermissions[$datum->name]['permissions'] = explode(', ', $datum->perm);
     +      $this->sourcePermissions[$datum->name]['lookup_up'] = $datum->rid;
     +    }
  6. This comment seems out of place now:

     @@ -51,75 +102,38 @@ protected function assertRole($id, array $permissions, $lookupId, MigrateIdMapIn
     ...
        protected function assertRoles(MigrateIdMapInterface $id_map) {
     -
          // The permissions for each role are found in the two tables in the Drupal 6
          // source database. One is the permission table and the other is the
          // filter_format table.
     -    $permissions = [
     -      // From permission table.
     -      'access content',
     -      'migrate test anonymous permission',
     -      // From filter_format tables.
     -      'use text format filtered_html',
     -    ];

    Maybe just delete the comment. Maybe replace it with one in the setUp() function, explaining that permissions from the filter_format table are not migrated when validate_permissions is set and the filter_format migration has not been run.

  7. I think this can be simplified:

     +  public function assertMessages($expected_messages, $id_map) {
     +    $messages = $id_map->getMessages();
     +    $count = 0;
     +    foreach ($messages as $message) {
     +      $src_rid = $message->src_rid;
     +      $this->assertSame($message->message, $expected_messages[$src_rid]);
     +      $this->assertSame(MigrationInterface::MESSAGE_WARNING, (int) $message->level);
     +      $count++;
     +    }
     +    $this->assertSame(count($expected_messages), $count);
     +  }

    Why not loop through the messages, testing the message level as you go but then $actual_messages[$message->src_rid] = $message->message;? Then, after the loop, assert that the two arrays are the same.

  8. Same function: we can use string[] instead of array in the @param comment for $expected_messages.

  9. Same function: the function signature has no type declarations.

  10. This comment seems off point now that we only filter permissions if validate_permissions is set:

     @@ -129,6 +143,46 @@ public function testUserRole() {
     ...
     +  public function testUserRole() {
     ...
     +    // After the d6_filter_format migration, there are changes to the
     +    // permissions.
  11. These long lines are hard to read. What do you think of helper functions to merge and diff with $this->expectedPermissions? The helper function could also handle translating role names. Or maybe it can just take source and destination roles as arguments.

     +    $this->expectedPermissions['anonymous']['permissions'] = array_merge($this->sourcePermissions['anonymous user']['permissions'], [
     +      'use text format filtered_html',
     +    ]);
  12. Do we need shortcut in the D6 test? My list in #41 includes d7_shortcut_set but not d6_shortcut_set.

     @@ -162,6 +217,126 @@ public function testUserRole() {
     ...
     +  public function testUserRoleWithPermissions() {
     +    // Install modules that have migrations that may cause permissions
     +    // to be created.
     ...
     +      'shortcut',
  13. What about d6_node_type?

     +    // Run all the migrations.
     +    $this->installEntitySchema('block_content');
     +    $this->installConfig(['block_content', 'comment']);
     +    $this->migrateContentTypes();
     +    $this->executeMigrations([
     +      'd6_comment_type',
     +      'block_content_type',
     +      'contact_category',
     +      'd6_filter_format',
     +      'd6_taxonomy_vocabulary',
     +      'd6_taxonomy_vocabulary_translation',
     +    ]);
  14. Didn’t we deprecate $migration->set()? No, I guess I owe you a review on #2796755: [PP-1] Deprecate Migration::set().

     +    // Create the user role migration with validate_permissions set.
     +    $migration = $this->getMigration('d6_user_role');
     +    $destination = $migration->getDestinationConfiguration();
     +    $destination['validate_permissions'] = TRUE;
     +    $migration->set('destination', $destination);
     +    $this->executeMigration($migration);
quietone’s picture

Status: Needs work » Needs review
FileSize
21.14 KB
28.61 KB

@benjifisher, good to know we are getting close. I had intended to comment that I wanted to do a self review but obviously forgot. You picked up quite a few of the things wanted to improve.

1,2,4,8,9 Fixed
5. changed lookup_up to rid.
6. Instead of setup() I put it in the class docs. If I were new to this that is where I would like to find that information.
7. Changes applied to both d6 and d7. And removed $count, there isn't a need to test that anymore.
10. d6_user_role has an existing required dependency on d6_filter_format so this comment is currently true. I guess that should change as well. Adding this to the todo
11. Added a helper to set the expected permissions. I wasn't sure about the formatting just did something I like.
12. copy/paste error. Fixed.
13. d6_node_type is executed in the helper $this->migrateContentTypes();
14. Yes I was thinking this would be changing in the near future.

TODO 3, 10

quietone’s picture

#43.3 Further to needing 'locale' installed on three tests that previously didn't needed it. Poked around a bit and saw that each of those tests installs config_translation which has a dependency on 'locale'. locale also adds a permission. When getting the permission the permissionHandler is looking for the service 'locale.config_manager' which, of course, does not exist and results in a fatal error. Installing 'locale' fixes that.

There are other Kernel migration tests that install config_translation and not locale and they work fine because they are not doing anything that requires the locale.config_manager.

benjifisher’s picture

@quietone:

Nice work! I especially appreciate the helper setDestPermissions(). When I looked at the patch from #42, I did not even realize that the array_merge() and the array_diff() were adding to/removing from the same arrays. Long lines really are hard to understand.

I see you already fixed a lot of minor issues in the D7 test.

And thanks for tracking down the dependency on the locale module. That gives me an idea …

  1. Let’s initialize $destinationPermissions = [] and then wrap the middle two lines in if (!empty($configuration['validate_permissions'])). Then we only generate the list of permissions when we need it. As I said before, it is moderately expensive. And we can remove the locale that we added to the existing tests. (I tried one of them.)

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,59 @@
     +  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
     +    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition, $migration);
     +    $permission_handler = $container->get('user.permissions');
     +    $instance->destinationPermissions = array_keys($permission_handler->getPermissions());
     +    return $instance;
     +  }
  2. What do you think of renaming this helper function to expectPermissions()?

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -162,6 +232,156 @@ public function testUserRole() {
     ...
     +  public function setDestPermissions($role, array $additions, array $deletions) {
  3. Same function: if we add the default value $deletions = [], then we can leave off that argument in 5 of the places it is called.

  4. Same function: can we move it to the top of the file, with the other helpers?

  5. I think we can move the sort() to the setUp() function. We mostly use assertEntity() with the permission arrays set there. I think there are just two exceptions, and they both start with those permissions and then remove some with array_diff(). There was another recent issue where we discussed this: array_diff() does not change the order.

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php
     @@ -29,34 +47,39 @@ protected function setUp(): void {
     ...
     -  protected function assertEntity($id, $label, $original_rid) {
     +  protected function assertEntity($id, $label, array $expected_permissions) {
          /** @var \Drupal\user\RoleInterface $entity */
          $entity = Role::load($id);
          $this->assertInstanceOf(RoleInterface::class, $entity);
          $this->assertSame($label, $entity->label());
     +    sort($expected_permissions);
     +    $this->assertSame($expected_permissions, $entity->getPermissions());
     +  }
benjifisher’s picture

I forgot to set the status to NW. While I am at it, let me see if the issue summary needs any attention ... also the change record.

quietone’s picture

Status: Needs work » Needs review

Another installment...

#46
1. Yes, that should done. Fixed. Removed local from MigrateTermNodeTranslationTest, still needed in the block tests.
2. Name changed.
3. 3rd parameter defaults to [] now and 3rd parameter removed from calls as needed.
4. Yes, moved. Oops, it is there because I prefer helpers at the end of the file.
5. Moved the sort but still need one in UserRoleWithPermissions. Still, it is less sorting overall.

quietone’s picture

It helps to upload the patch.

benjifisher’s picture

Issue summary: View changes

I updated the change record, and I copied the first line of my draft as the release-notes snippet here.

benjifisher’s picture

I think we want !empty($configuration['validate_permissions']), not empty($instance->destinationPermissions):

    +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
    @@ -0,0 +1,61 @@
    +  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
    +    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition, $migration);
    +    if (empty($instance->destinationPermissions)) {
    +      $permission_handler = $container->get('user.permissions');
    +      $instance->destinationPermissions = array_keys($permission_handler->getPermissions());
    +    }
    +    return $instance;
    +  }

I have a feeling that the test in #48 is going to fail. If we get this straight, then I do not see why we need locale in the block tests.

Update: I take it back: the tests will not fail, but we will always calculate the permissions, whether we use them or not.

benjifisher’s picture

This is what I meant in #46.5: if $this->sourcePermissions[3] is already sorted (in setUp()), then $admin_permissions does not need to be sorted.

    +    // Remove permissions not on the destination site.
    +    $admin_permissions = array_diff($this->sourcePermissions[3], [
    +      'access all views',
    ...
    +      'view revisions',
    +    ]);
    +    sort($admin_permissions);
quietone’s picture

This will be very brief as I have an appointment.

#51 What a ditz I am. Fixed now and also the Block tests.
#52. The array_diff preserves the keys which means it will not match. The sorting process fixes that.

Edit: s/not/now

The last submitted patch, 49: 2953111-48.patch, failed testing. View results

quietone’s picture

Instead of sort use array_values.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

@quietone:

Do not be too hard on yourself. I often say that even smart people make dumb mistakes. I also usually blame multitasking.

I am glad we can remove the changes to unrelated tests. Such changes are always reason for skepticism.

Good call on using array_values()!

The patch in #55 fixes the last of my concerns: RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 55: 2953111-55.patch, failed testing. View results

benjifisher’s picture

I just added #3207086: [HEAD BROKEN] Consistent failure in MonthDatePluginTest for the unrelated test failure. Let's see if there is any response on that issue before we ask the testbot to reconsider.

If the testbot just switched to DST, then a lot of tests will fail, and we do not want to queue them all up at once.

Spokje’s picture

Status: Needs work » Reviewed & tested by the community

#3207086: [HEAD BROKEN] Consistent failure in MonthDatePluginTest was committed. Ordered retest and put this issue back to RTBC per #56.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 55: 2953111-55.patch, failed testing. View results

benjifisher’s picture

Status: Needs work » Reviewed & tested by the community

Now a different unrelated test is failing!

I reported the failure at #3207125: [random test failure] SettingsTrayBlockFormTest:: testEditModeEnableDisable(), and I am queuing another re-test.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/user/migrations/d6_user_role.yml
@@ -37,6 +37,14 @@ process:
+  validate_permissions: false

+++ b/core/modules/user/migrations/d7_user_role.yml
@@ -35,6 +35,14 @@ process:
+  validate_permissions: false

I think this should be true for new migrations. Existing migrations will not have this key and therefore will use the existing behaviour but new migrations should benefit from this change today without having to do additional steps.

Existing migrations will have already copied the template and not have the key so I don't think we need to be concerned about them. If someone is starting fresh and doing repeated core upgrades from D6 or D7 while also upgrading there codebase then they are doing that because they want the latest migration bug fixes of which this is one.

alexpott’s picture

+++ b/core/modules/user/migrations/d6_user_role.yml
@@ -37,6 +37,14 @@ process:
+  optional:
+    - block_content_type
+    - contact_category
+    - d6_comment_type
+    - d6_node_type
+    - d6_taxonomy_vocabulary
+    - d6_taxonomy_vocabulary_translation

+++ b/core/modules/user/migrations/d7_user_role.yml
@@ -35,6 +35,14 @@ process:
   optional:
+    - block_content_type
+    - contact_category
+    - d7_comment_type
     - d7_filter_format
+    - d7_node_type
+    - d7_shortcut_set
+    - d7_taxonomy_vocabulary
+    - d7_taxonomy_vocabulary_translation

This bit kinda concerns me though. We're adding these because they create dynamic permissions and so the role migration needs to depend on that. But it's possible for contrib to have dynamic permissions like this to. How is it going to work for such a module if it provides a migration? For example, what's the impact of this change on commerce migrations? I'm pretty sure that set of modules has some dynamic permissions.

alexpott’s picture

I think in an ideal world permissions would be fixed up after all the migrations have been run and the migration is finished. This would be kinda of what would happen if #2571235: [regression] Roles should depend on objects that are building the granted permissions lands and someone does a migration and then goes to the user permissions screen and presses save. But we don't have the ability to set post migration tasks do we?

benjifisher’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs change record +Needs change record updates

@alexpott:

Thanks for taking a look!

Re #62:

I agree with changing the default from false to true, but I do not think we should do that until Drupal 10. This is potentially disruptive: not only may it affect current migrations, but an experienced migrator might start a new project and expect things to work as they always have. We have already made BC breaks in the migration system, like the one fixed by #3184650: ContentEntity migration source adds revision ID as source key, incompatible with Drupal 8.8 and earlier.

Existing migrations will have already copied the template and not have the key …

That applies to projects that use config entities to represent migrations, supported by the migrate_plus module. That is not the only way to run migrations. There is the core UI, the migrate_run module, and Drush 10.4.

If someone is … doing repeated core upgrades … then they are doing that because they want the latest migration bug fixes of which this is one.

When I start a new project, I like to target the version of Drupal that will be current when the project is ready to launch. If I were to start one today, then I would use 9.2.x. There are lots of reasons I might want to update that are unrelated to migrations.

Re #63:

I think we have done everything we can in core to handle the dependencies. A contrib module that provides migrations will probably need to add dependencies.

What can we do to make this easier for the maintainers? We should probably add some code samples to the change record. Anything else?

This seems like another good reason to keep the BC default until Drupal 10.

Luckily, one of the maintainers of the commerce_migrate module is already involved with this issue!

Re #64:

[W]e don’t have the ability to set post migration tasks do we?

It depends on what you mean by "post migration". We have MigrateEvents::POST_IMPORT: see the change record. I do not think that has any advantage over changing the destination plugin, which is what we do here.

If you mean when the migrations are "done done" and the site is ready to go live, then there is no way to tell. Incremental migrations can be done even after the site goes live.

To recap:

  1. I agree with changing the default, but I recommend waiting until Drupal 10.
  2. We should update the change record with sample code for adding a dependency to the user-role migrations.
  3. Rider: also mention in the CR that we log the removed permissions!

If the current patch is committed, then it will be easy to change the default in a later issue. The hard part will be the documentation.

If I still have not convinced you, then I am willing to go along with changing the default now.

benjifisher’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

It depends on what you mean by "post migration". We have MigrateEvents::POST_IMPORT: see the change record. I do not think that has any advantage over changing the destination plugin, which is what we do here.

Yeah that doesn't offer any advantage. We need is something that will run once a complete set of migrations is finished. What I think would work best is if there was some role cleanup migration that always depends on all the existing migrations - this step will remove any permissions which doesn't exist at this point. So I think we leave the existing role migration as it is but somehow introduce another migration whose sole purpose is to fix the permissions and run after all other migrations have finished.

However I don't think we can do this until we've landed #2571235: [regression] Roles should depend on objects that are building the granted permissions - since that'll mean all this step will need to do is to re-save the role without the migrating flag. While is #2571235 is outstanding and we're dealing with D6 and D7 data I think attempts to make this stricter are likely to run into hard to predict edge cases. If you look at user_role_change_permissions() works and \Drupal\user\Form\UserPermissionsForm::submitForm() it's like the system is designed to work with permissions that are not declared to the system.

benjifisher’s picture

From #67:

We need is something that will run once a complete set of migrations is finished.

That is what I was thinking of when I said (in #65), "... there is no way to tell. Incremental migrations can be done even after the site goes live."

I just read a description of an upcoming DrupalCon session. It talked about how bad content keeps getting migrated from one template to another, and no one ever cleans it up. The same thing happens with permissions. At least, this issue (in its current form) gives the developer working on the migration the option to break that cycle.

Perhaps we should

  1. Demote this issue from Critical to Normal.
  2. Update the title and summary.
  3. Consider this issue on its own merit.
  4. Reopen #3055557: Role migration results in migration permissions that do not exist on the target site with something like #67 as its proposed resolution.
benjifisher’s picture

Priority: Critical » Normal
Issue summary: View changes
Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs release note, -Needs change record updates

As I suggested in #68, I am demoting this issue from Critical to Normal. I am also cleaning up the issue tags.

I updated the issue summary and the change record, mentioning that we log messages when removing permissions.

@alexpott, it seems to me that what you are asking for is a different, but related, feature. I do not see that as a reason to postpone this issue. We can reopen #3055557: Role migration results in migration permissions that do not exist on the target site or open a new issue to do what you want, postponed on #2571235: [regression] Roles should depend on objects that are building the granted permissions if that is appropriate.

... somehow introduce another migration whose sole purpose is to fix the permissions and run after all other migrations have finished.

I still do not see how that is supposed to work. We can discuss that on another issue, or at one of the weekly migration meetings. I just added a note to #3207879: [meeting] Migrate Meeting 2021-04-22.

... I think attempts to make this stricter are likely to run into hard to predict edge cases.

I am not especially worried about the edge cases. The new option is opt-in, and we log a message for any role that has permissions removed. Let's give developers this option and let them tell us what the edge cases are!

benjifisher’s picture

I finally noticed that #67 refers to "the migrating flag." What flag is that? I see that it is added as part of #2571235: [regression] Roles should depend on objects that are building the granted permissions. I think I will leave a comment on that issue.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

As stated in #67 I don't think this is rtbc. The solution of adding dependencies to the roles migrations doesn't work because core doesn't yet enforce any sort of dependencies on permissions - they don't even have to exist in permissions.yml or a callback in order to be used! So it's perfectly possible for a role have a permissions and it to be checked in code and for the permission to not be declared. We have tests in HEAD that rely on this behaviour - see #2571235: [regression] Roles should depend on objects that are building the granted permissions. I think we need another approach. I think perhaps the way forward is to use a special configuration for migrate that stores permissions that do not exist at the time of role migration. These permissions are declared as real permissions by the user module under a special migrated permission section. As we go through migrations we have MigrateEvents::POST_IMPORT event that cleans up these permissions as they come to exist. If we combine that approach with #2571235: [regression] Roles should depend on objects that are building the granted permissions think I think we'll get to a place where we can deprecate support for permissions that don't exist and solve the critical bugs that exist because we don't tidy up after ourselves.

benjifisher’s picture

Title: Only migrate role permissions that exist on the destination » [PP-1] Only migrate role permissions that exist on the destination
Priority: Normal » Critical
Issue summary: View changes
Status: Needs work » Postponed

Now that I have looked at #2571235: [regression] Roles should depend on objects that are building the granted permissions, I think the right action is to postpone this issue until that one is fixed.

I am doing that and also restoring the Critical status of this issue. I will also make some updates to the issue summary.

quietone’s picture

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

benjifisher’s picture

Title: [PP-1] Only migrate role permissions that exist on the destination » Only migrate role permissions that exist on the destination
Issue summary: View changes
Issue tags: +Needs issue summary update

#2571235: [regression] Roles should depend on objects that are building the granted permissions has landed, so we can return to this issue.

Given the changes in #2571235, we will have to reconsider the proposed resolution, so I am adding the tag for an IS update.

benjifisher’s picture

Status: Postponed » Needs work
quietone’s picture

Restarting by making a test only patch from the latest patch, including fixing a typo. No need to run tests right now.

mikelutz’s picture

Re #64:

[W]e don’t have the ability to set post migration tasks do we?

It depends on what you mean by "post migration". We have MigrateEvents::POST_IMPORT: see the change record. I do not think that has any advantage over changing the destination plugin, which is what we do here.

If you mean when the migrations are "done done" and the site is ready to go live, then there is no way to tell. Incremental migrations can be done even after the site goes live.

At the core level, we really only need to worry about this for the migrate_drupal_ui module. Such a cleanup can be run at the end of the batch process in that module. Running incremental migrations, or migrations one at time or with drush, or other developer-y things that developers do to migrate a site in the real world end up being out of our control, and there are infinite ways to use the migration system to do bad things if you have developer access.

quietone’s picture

There are also 'Follow-up Migrations'

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

quietone’s picture

I am not sure how to proceed on this.

For the Migrate UI, do we want to only migrate roles that exist on a row by row basis or migrate all the roles and clean the up at the end.

For those migrating with other tools. What support is needed? Will they need a way to remove migrated roles if they are migrates as per #71.

danflanagan8’s picture

For those migrating with other tools. What support is needed? Will they need a way to remove migrated roles if they are migrates as per #71.

This is an interesting question! I was looking at the core issue that added dependencies to roles (#2571235: [regression] Roles should depend on objects that are building the granted permissions) to see how they handled cleaning up existing roles. They have a post_update function that does all the cleanup:

https://git.drupalcode.org/project/drupal/-/blob/9.4.x/core/modules/user...

To me it seems like from the migrate perspective, we just need to come up with a way to call that function (or a copy of it) after the migration is complete. Sounds like @mikelutz things we can put that at the end of the batch that migrate_drupal_ui uses.

From a contrib standpoint, maybe migrate-tools gets a new drush command to call the update?

quietone’s picture

Status: Needs work » Needs review

I am not convinced of that. If the permission is not migrated in the first place then there is no need for cleanup. That is what the destination plugin does in #55, when validate_permissions is enabled on the destination.

mikelutz’s picture

Status: Needs review » Needs work

Right, but the issue that @alexpott brought up is that #55 makes the D7_user_role migration dependent on any core migrations that may create dynamic permissions, but it can't do that for contrib modules that might have migrations that create dynamic permissions. So if we go with #55 and user_role runs before products for example, it will strip out per product permissions because they don't yet exists. (This is an off the top of my head example, I don't recall the permission structure of commerce, but the idea is the same in general.) Since we can't make D7_user_role depend on any random contrib migrations that would have to run first to create dynamic permissions, the only choice we have is to migrate all the permissions and then clean up when the whole upgrade process is done.

quietone’s picture

Status: Needs work » Needs review
FileSize
21.29 KB

@mikelutz, thanks. that all makes sense.

Here is the start of a new patch, working to implement #67, where the permissions that do not exist at the time of the user_role migration are created as temporary permissions. When a migration is done via /upgrade these temporary permissions are removed and the roles updated when the migration is complete. The roles are updated using an exact copy of user_post_update_update_roles(). When a migration is run by other means the temporary permissions are not removed but since they are stored in config they can easily be accessed and removed.

So, is this now moving in the right direction?

No interdiff because of the new approach.

quietone’s picture

oops, I wasn't working from HEAD

Status: Needs review » Needs work

The last submitted patch, 86: 2953111-86.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
1.74 KB
21.24 KB

Instead of adding more changes, this moves the abstract method declaration. However, it is inconsistent with the way the getEntityCounts() is defined but this should allow the tests to pass. It can be changed later to be consistent if that is what we want.

quietone’s picture

Version: 9.4.x-dev » 10.0.x-dev
Issue summary: View changes
FileSize
1.4 KB
21.05 KB

Needed a reroll and moving to D10

Status: Needs review » Needs work

The last submitted patch, 89: 2953111-89.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
1.21 KB
22.63 KB

And update the new Upgrade tests in aggregator.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Needs work

This is an interesting approach. I was not thinking that the Migrate module would actually define (or provide) permissions, but I guess that is what we have to do.

I think we should expand the "Proposed resolution" section in the issue summary. Be more explicit about how we add permissions and how we remove them, and how the user roles get updated. I can do that, but not at this hour. I see this issue already has the tag for an issue summary update. +1

  1. It is a problem if these temporary permissions are never removed. That should not happen with migrate_drupal_ui, but it could happen if an error interrupts the process before the cleanup stage. At least, I think that could happen. And it is certainly a possibility for custom migrations. So let's implement hook_requirements() and add a warning to the site status report if any temporary permissions are defined in the new config object.
  2. In #82 and #85, we discussed how to update the user roles. I did not comment here, but I think I may have endorsed this approach in Slack, possible during one of the weekly meetings. I am changing my mind.

    There is a simpler way. Make all of the temporary permissions depend on the new config object. See Permissions can define dependencies, one of the change records for #2571235: [regression] Roles should depend on objects that are building the granted permissions. We do not need the new trait mentioned there. Just add the dependencies in nonExistentPermissions(). Then, whenever the config object is updated, the config dependency system will take care of updating the roles. (I am pretty sure that is true. We need to confirm!)

    This also makes it really easy for a contrib or custom module to clean up the permissions: just delete the temporary permissions.

  3. A natural extension of the first two points is that we should add a settings form in the admin UI, listing the temporary permissions and allowing the site admin to delete them. There is no need to provide a complete CRUD UI: just a button to remove them all. Then the warning on the status report can be actionable: provide a link to the new page.

    Actually, the permissions are already listed on /admin/people/permissions/module/migrate. Would it make sense to add a link there to a confirmation form?

  4. If I am right about the config dependency system, then we do not need updateRoles(). We might still want it, in order to provide logging. If so, and if I am right about dependencies, then we have to call the function before updating the config. But if this method is really an exact copy of user_post_update_update_roles(), can we just call that function instead? Maybe not: I guess that update functions are eventually removed.
  5. When we migrate a non-existent permission to a role, we add the temporary permission to the config item and we add the permission to the role. The role should get a dependency on the migrate module automatically. If you follow my earlier suggestion, then the role should also depend on the config item. If the migrate module is uninstalled, or if the permission is removed from the config object, then the role should be updated (removing the permission).

    I think all of that is pretty good DX.

    I would like to see test coverage of all of that. If we test removing a role from the config object, then maybe we do not also have to test uninstalling the module.

  6. +  /**
    +   * Gets roles with non-existent permissions removed.
    +   *
    +   * @return string
    +   *   A list of roles that have had permissions removed.
    +   */
    +  protected function getRoles() {
    +    return '';
    +  }
    +
    +  /**
    +   * Gets roles with non-existent permissions removed post incremental.
    +   *
    +   * @return string
    +   *   A list of roles that have had permissions removed.
    +   */
    +  protected function getRolesIncremental() {
    +    return '';
    +  }
    

    Nit: Can we add "comma separated" to the @return comments?

  7. Same request (nit) for some of the helper functions in the tests.
  8. -  protected function assertUpgrade(array $entity_counts) {
    +  protected function assertUpgrade(array $entity_counts, $roles) {
         $session = $this->assertSession();
         $session->pageTextContains(t('Congratulations, you upgraded Drupal!'));
    +    if ($roles) {
    +      $regex = '/The role[s]? ' . $roles . ' have|has had non-existent permissions removed/';
    +      $session->pageTextMatches($regex);
    +    }
    

    The brackets are optional in [s]?. If you think they make the regexp easier to read, then it is worth keeping them. I think we need to group the alternation: (have|has). I am nervous about inserting a variable into a regular expression. I guess it is OK, since it is a comma-separated list of machine names. Can we just apply preg_quote() to be safe?

  9. Is it worth adding a description to each permission? We would have to store it in the config item and retrieve it in the permissions callback. I was thinking that it might be useful to know which migration(s) added the permission, but maybe we do not have access to that in the destination plugin.

Naming things is always hard. I will not insist on any of these changes. They are just suggestions.

  1. Use "temporary permissions" instead of "non-existent permissions": in the config schema, Permissions method name, doc blocks and other code comments, variable names, and log and Migrate messages.
  2. Maybe use getTemporaryRoles() in the tests instead of getRoles().
  3. Instead of the migrate.permission config item with the key permissions, I would use migrate.settings with the key temporary_permissions.
  4. In #40 on this issue, I listed the modules that have permissions callbacks. The classes are not consistently named, but they usually follow the pattern ModuleNamePermissions. Maybe we should use MigratePermissions.
  5. I think that removeTemporaryPermissions() is clearer than updateRoles().
benjifisher’s picture

#92.3: I guess the simplest thing to do is add links to the warning message (on the site status report) to the Migrate permissions page and to a confirmation form. Other than the confirmation form, we do not need a new page.

In the new destination plugin, it is a little confusing to use the same variable for a string and then for an array:

+      $non_existent_permissions = print_r($diff, TRUE);
+      $message = "Permission(s) '$non_existent_permissions' not found.";
+      $this->migration->getIdMap()
+        ->saveMessage($row->getSourceIdValues(), $message, MigrationInterface::MESSAGE_WARNING);
+
+      // Save the non-existent permissions in config so they can be added as
+      // permissions by the migrate module.
+      // @see \Drupal\migrate\Permissions
+      $config = $this->configFactory->getEditable('migrate.permission');
+      $non_existent_permissions = $config->get('permissions') ?? [];
+      $config->set('permissions', array_values(array_unique(array_merge($diff, $non_existent_permissions))));

The first two times I looked at this, I thought we were passing a string as the second argument of array_merge().

Maybe just eliminate the first variable:

$message = 'Permission(s) ' . print_r($diff, TRUE) . ' not found.';

or something similar with sprintf().

quietone’s picture

Status: Needs work » Needs review
FileSize
16.98 KB
21.94 KB

@benjifisher, thanks for the review and the renaming suggestions. I recall not liking the names at the time but I was focused on the functionality.

It is late and I only worked on the simpler points and the renaming.

#92. 6, 7 and 8 Fixed. Also all the renaming points in the final paragraph.

TODO. #92 1 -> 5 and 9. # 93

benjifisher’s picture

@quietone, thanks for keeping this moving!

By the way, having a patch to review is a huge help. In principle, I could have made those suggestions before you did any work, but having a patch to review gives me a lot more focus.

After sleeping on it, it occurs to me that we are not thinking about the iterative nature of migration development. We work on the migrations and the site building in parallel. For this issue, that means we have to consider what happens when we migrate some user roles, then enable one or two modules that define "missing" or "temporary" permissions, then repeat the process.

What happens when two modules define the same permission? I do not know. The permission machine names are the array keys of $permission_handler->getPermissions(), so that function will return only one of them.

I was thinking that the destination plugin should recalculate the temporary permissions from scratch, instead of just adding to them. But I was wrong: the destination plugin is looking at just one role at a time. It could check the existing temporary permissions, and remove the ones that are defined by another module. But maybe it does not have to: see the next point.

I think what we have to do is react whenever a module is enabled. There is a hook for that, isn't there? Or maybe an event. Check the permissions it defines, including its permissions callback. Because of the array-key issue I mentioned before, we cannot rely on $permission_handler->getPermissions(). If any of those permissions are defined by the Migrate module, then remove them.

quietone’s picture

it occurs to me that we are not thinking about the iterative nature of migration development.

That it true. mikelutz provides an answer in #78 which is that we only have to handle the case of /upgrade. And, right now, I that is correct for the scope of this issue. Exploring implications with iterative nature of migration can be in a separate issue? However, I just realized that there is nothing in the current patch that checks what happens to the temporary permissions when the user role migration is rolled back. Added that to the todo list at the end of this comment.

What happens when two modules define the same permission?

Ah, I stumbled on this while working on this issue. I reckon this needs an issue, if there isn't one already.

This patch adds information to status report page (#92.1), which will need work. It should at least link to somewhere where the user can learn how to remove the permissions. Also, it removes the check for skipping the missing permission deprecation in Entity/Role.php that was accidentally left in the previous patch.

TODO. #92 1 -> 5 and 9. # 93, test rollback

Status: Needs review » Needs work

The last submitted patch, 96: 2953111-96.patch, failed testing. View results

quietone’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
11.07 KB
5.12 KB
27.82 KB

#92. 2 and 9. The destination plugin EntityUserRole now adds a title, description and dependency on 'migrate' to the stored permissions. This required a few tweaks in the tests and an expanded schema file. The description can and does include the migration id as can be seen in this screenshot of /admin/people/permissions/module/migrate. Since it is very likely that there is only one migration of user roles, it doesn't seem very useful to me. Viewing the permissions is not yet available with this patch. I did it by running a Drupal 6 upgrade with some code commented out so the permissions were not removed.

I did uninstall migrate and only some of the permissions were removed but I need to retest that.

quietone’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

When I uninstalled migrate I got the confirmation screen about what config was to be updated and when I uninstalled all the migrate permissions were removed. So, that is good. I also noted that on the module page that migrate had a permission link when there were permissions.

But I found another thing to look into, when there are no migrate permission navigating to /admin/people/permissions/module/migrate results in a redirect loop.

And thinking about a form to allow deleting the temporary permissions, right now I think that would be better in a followup.

Did some work on the IS so removing tag.

Status: Needs review » Needs work

The last submitted patch, 98: 2953111-99.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
1.71 KB
27.92 KB

I forgot to update the status page test due to the changes to the config schema.

benjifisher’s picture

... when there are no migrate permission navigating to /admin/people/permissions/module/migrate results in a redirect loop.

What is supposed to happen is a 403 response, and the link should not appear on /admin/modules.

I tested this, and it seems to work as designed. I applied the patch from #101 and installed the Standard profile. The "Migrate permissions page" means /admin/people/permissions/module/migrate.

  1. Log in as admin.
  2. Visit the Migrate permissions page. Access denied (403 response).
  3. Use drush config:import --partial --source=config to import the YAML file below, saved as config/migrate.settings.yml.
  4. Reload the Migrate permissions page. It shows the permission as expected.
  5. Use drush config:edit migrate.settings to set temporary_permissions: [].
  6. Reload the Migrate permissions page. Access denied (403 response).

I was afraid that the access result was cached incorrectly, so I did not clear caches during this test.

Can you give steps to reproduce the redirect loop?

Here is my YAML file:

temporary_permissions:
  - title: Test permission
    description: Pretend that this permission was migrated from somewhere.
    dependencies:
      config:
        - migrate.settings
benjifisher’s picture

I did a quick review of the changes inside the migrate module. It looks good so far! I just have a few comments and really minor suggestions.

  1. I guess that StringTranslationTrait provides both t() and formatPlural(), but procedural code only has the first. Oh, well.
  2. Do we really need an inline template for the requirements? Can't we just use an existing TranslatableMarkup object as the replacement for a token in t()? Or, as I suggested in #93, we do not have to work so hard here. Since the permissions are already listed on the Migrate permissions page, we can give a link to that instead of generating the list here.
  3. The permissions callback does not need an editable config object.
  4. I was thinking of adding dependencies in the permissions callback, but adding them to the config object should work, too. It might even be handy to let developers modify the permissions with config import/export.
  5. If you eliminate the variable, then the permissions callback becomes a one-liner.
benjifisher’s picture

I looked at the changes inside the user module. Again, just a few minor points.

  1. The destination plugin extends EntityConfigBase, which extends Entity (abstract destination plugin). I am surprised that this base class has $storage and $bundles properties, and these have to be supplied by the constructor. I would expect those to be in EntityContentBase. Can config entity types have bundles?
  2. Initialize $temporary_permissions = []; at the top of the function, outside the if() block, so that it is always defined when you get to the last block of the function.
  3. In the d6 test, it looks as though $provided_by_migrate is defined and then never used. Am I missing something?

Edit: to answer my own question, yes, config entities can have bundles. So I guess it does make sense for the Entity class to have those properties.

benjifisher’s picture

Status: Needs review » Needs work

I looked at the remaining changes: in the aggregator and migrate_drupal_ui modules. I think there are a couple of things that should be changed.

  1. I know this is a direct copy of the post-update function, but I think we let a goof slip through:

     +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php
     @@ -367,4 +379,40 @@ public static function onIdMapMessage(MigrateIdMapMessageEvent $event) {
     +    \Drupal::classResolver(ConfigEntityUpdater::class)
     +      ->update($sandbox, 'user_role', function (Role $role) use ($existing_permissions, &$cleaned_roles) {
     +        $removed_permissions = array_diff($role->getPermissions(), $existing_permissions);
     +        if (!empty($removed_permissions)) {
     +          $cleaned_roles[] = $role->label();
     +          \Drupal::logger('update')->notice(
     +            'The role %role has had the following temporary permission(s) removed: %permissions.',
     +            [
     +              '%role' => $role->label(),
     +              '%permissions' => implode(', ', $removed_permissions),
     +            ]
     +          );
     +        }
     +        $permissions = array_intersect($role->getPermissions(), $existing_permissions);
     +        $role->set('permissions', $permissions);
     +        return TRUE;
     +      });

    The last three lines should be inside the if block. If there are no removed permissions, then there is no need to update the role.

  2. You missed one doc block that can use "comma separated":

     +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php
     @@ -22,6 +22,26 @@ protected function setUp(): void {
     ...
     +  /**
     +   * Gets roles with temporary permissions removed.
     +   *
     +   * @return string
     +   *   A list of roles that have had permissions removed.
     +   */
     +  protected function getTemporaryRoles() {
  3. If the method returns '', then there is no need to
    override the base implementation.

     +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php
     @@ -190,6 +190,20 @@ protected function getMissingPaths() {
     ...
     +  /**
     +   * {@inheritdoc}
     +   */
     +  protected function getTemporaryRolesIncremental() {
     +    return '';
     +  }
     +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php
     @@ -217,6 +217,20 @@ protected function getMissingPaths() {
     ...
     +  /**
     +   * {@inheritdoc}
     +   */
     +  protected function getTemporaryRoles() {
     +    return '';
     +  }
     +
     +  /**
     +   * {@inheritdoc}
     +   */
     +  protected function getTemporaryRolesIncremental() {
     +    return '';
     +  }

There are still a couple of to-dos from #92, right? If you want to leave the cleanup form to a follow-up issue, then I think I can implement it.

ravi.shankar’s picture

Addressed point 1 and 3 of comment #105, still needs work for others.

benjifisher’s picture

@ravi.shankar:

Thanks for helping with those points. Are you planning to continue working on this issue?

quietone’s picture

Status: Needs work » Needs review
FileSize
3.92 KB
26.49 KB

I went through all the remaining points to determine what is still to do. I wanted to make a summary but I don't have the brain power for that now. I should be able to have another look at this in the next two days.

Updated patch with some fixes.

#92
1. Fixed, hook_requirements is in migrate.install
2. Fixed in #98
3. I agree that a UI is needed for deleting the temporary permissions. Creating such things is a weak point for me but adding a button to the migrate permission page seems wrong and inconsistent. I reckon a new form is needed.
4. Todo
5. Todo
9. Fixed in #98
93. Removed $temporay_permission variable that was used for the building of the log message.

#102. Todo Provide steps to reproduce the redirect loop

#103
1. Oh well indeed.
2. Todo
3. Fixed, no longer using editable config.
4. I did add the dependencies in the callback at first. But that split up writing to the config in two places and it seems
cleaner to do it in one place, the destination plugin.
5. Fixed permission callback is one liner.

#104
1. It does seem a bit odd, the bundle is the same name as the config entity.
2. No change needed due to #105.1.
3. Ah, that is dead code. The real test is at the end of the file, after the comment "Confirm that temporary permissions are stored.".

#105
1. Fixed in #106
2. Fixed.
3. Fixed in #106

todo: test rollback

Status: Needs review » Needs work

The last submitted patch, 108: 2953111-108.patch, failed testing. View results

yogeshmpawar’s picture

Status: Needs work » Needs review

Looks like test failures are unrelated.

quietone’s picture

@yogeshmpawar, thanks for confirming the failure was a random fail. If you restarted the tests, I think we can save resources and not rerun the tests on every failure. There is lots more work to be done here anyway.

This does #103.2, removes the inline template in hook_requirements. The message on the status report page could use improvement and now looks like this:

List of remaining items
#92. 4, 5
102. Provide steps to reproduce the redirect loop
test rollback
UI to remove temporary permission

Status: Needs review » Needs work

The last submitted patch, 111: 2953111-111.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
8.23 KB
27.9 KB

Another installment.

test rollback - The destination simply removes all the temporary permission.
92. 5 - Changes StatusTest to PermissionTest and, at the end, uninstalled migrate and confirmed the temporary permissions are gone.

List of remaining items
#92. 4
102. Provide steps to reproduce the redirect loop
UI to remove temporary permission

Status: Needs review » Needs work

The last submitted patch, 113: 2953111-113.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
2.46 KB
27.99 KB

Another random failure: Drupal\Tests\media_library\FunctionalJavascript\CKEditorIntegrationTest::testButton

And now for the remaining issues.
#92-4. MigrateUpgradeImportBatch::removeTemporaryPermissions. Yes, this is definitely needed for logging. I also think that this should
stay so that each role has the temporaryPermissions removed at the end of the upgrade. I fixed the method so it is now comparing with the temporary permission and removing said permission after the roles are updated.

102. Provide steps to reproduce the redirect loop. I am not able to reproduce this so perhaps just a wonky test site. This is not for this issue but when there are no permissions I get an access denied message instead of a some nice message that there are no permissions.

And the last remaining task is to provide a UI to remove the temporary permissions. I still think that is outside the scope of this issue. And, if the tests are correct, we know that the temporary permissions will be removed when migrate is uninstalled.

Hopefully, all the points have had a response and we are all caught up.

And ready for the next round!

Status: Needs review » Needs work

The last submitted patch, 115: 2953111-115.patch, failed testing. View results

benjifisher’s picture

Issue summary: View changes

@quietone:

Yay, progress!

This is not for this issue but when there are no permissions I get an access denied message instead of a some nice message that there are no permissions.

That is by design.

The access checker means that we do not generate links to empty permissions forms. For example, /admin/modules already checks access for the Configure links; as of Drupal 9.3, it does the same for the Permissions links.

If you can think of a better way, let's talk about it ... on another issue. (As you said, "... not for this issue".)

I am not able to reproduce this so perhaps just a wonky test site.

If that were a separate issue, then we would close it as "cannot reproduce".

And the last remaining task is to provide a UI to remove the temporary permissions. I still think that is outside the scope of this issue.

I am willing to implement that in a follow-up issue.

I will do a full review in a couple of days. I already looked at the interdiff in #108, and that much looked reasonable.

quietone’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: +Needs followup
FileSize
4.57 KB
27.07 KB

@benjifisher, thanks. Yes, progress indeed, I am beginning to think the end is in sight!

Thanks for the explanation about the access checker. I am content to live with it as is. And ehile I do enjoy closing issues, after experience with Bug Smash I won't be making a new issue. ;-)

Adding tag for the followup.

Now for this patch. The test failed because temporary_permissions was expected to be an array and it was NULL. So, this add a config/install so that the temporary_permissions are initialized to an empty array. And I got a bit defensive and ensured that $temporary_permissions is never NULL by using '??'. This patch also adds some comments.

What is to do is to improve the requirements message. I have updated the remaining tasks.

benjifisher’s picture

Issue summary: View changes
Issue tags: -Needs followup

I added #3272352: Add a UI for deleting temporary permissions as a child issue, and assigned it to myself.

I think we should update the tile of this issue.

benjifisher’s picture

Issue summary: View changes

Oh, no! I must have forgotten to force-reload this page. I did not intend to make all those changes in the issue summary.

I am doing my best to restore the IS.

Status: Needs review » Needs work

The last submitted patch, 118: 2953111-118.patch, failed testing. View results

quietone’s picture

Issue summary: View changes
Status: Needs work » Needs review

This time the fail is Drupal\Tests\ckeditor5\FunctionalJavascript\MediaLibraryTest::testButton

I've added more tweaks to the IS but am blank on a new title, at least, right now.

benjifisher’s picture

Status: Needs review » Needs work

@quietone:

I think the patch here needs a reroll after #3264120: Remove aggregator module and our dependency on Laminas Feed.

Since you worked on that issue, I trust that you know whether we can just drop the changes for the tests in the Aggregator module or if those tests have moved somewhere else. Is there now a contrib Aggregator module that will need those changes?

benjifisher’s picture

Status: Needs work » Needs review

My bad: my local branch still had the changes to the Aggregator module, but it looks as though the patch in #118 already removed those changes.

benjifisher’s picture

Status: Needs review » Needs work
  1. I think we should also have an update function, or maybe post-update, to create the configuration object.

     +++ b/core/modules/migrate/config/install/migrate.settings.yml
     @@ -0,0 +1 @@
     +temporary_permissions: []
  2. If you change the key to temporary_permissions (plural) is it still NULL? I would think that the PHP object $config is not changed when the module is uninstalled.

     +++ b/core/modules/migrate/tests/src/Functional/System/PermissionTest.php
     @@ -0,0 +1,74 @@
     ...
     +    // Uninstall migrate and confirm the temporary permissions are removed.
     +    \Drupal::service('module_installer')->uninstall(['migrate']);
     +    $this->assertNull($config->get('temporary_permission'));
  3. I think the functional test will be even better if we

    1. Create a temporary permission using a migration.
    2. Check that the role has the temporary permission.
    3. Modify the config object, removing the permission.
    4. Re-check the role. See #92.5.
  4. I think this comment was copied from the post-update function. We are not calculating dependencies.

     +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php
     @@ -367,4 +379,48 @@ public static function onIdMapMessage(MigrateIdMapMessageEvent $event) {
     ...
     +  /**
     +   * Calculate role dependencies and remove temporary permissions.
     +   *
     +   * @return \Drupal\Core\StringTranslation\PluralTranslatableMarkup|void
     +   *   A message to display if temporary permissions were removed from any user
     +   *   role.
     +   */
     +  public static function removeTemporaryPermissions() {
  5. In fact, I think we can replace removeTemporaryPermissions() with a function that returns string[][]. The outer keys are role names, and the values are lists of removed permissions. Then, instead of

     +    // Remove the temporary permissions from the user roles.
     +    $message = MigrateUpgradeImportBatch::removeTemporaryPermissions();
     +    if ($message) {
     +      \Drupal::messenger()->addWarning($message);
     +    }
     +    // Remove the temporary permissions.
     +    $config = \Drupal::service('config.factory')->getEditable('migrate.settings');
     +    $config->set('temporary_permissions', []);
     +    $config->save();

    we can do something like

     $permissions = static::temporaryPermissionsByRole();
     // Remove the temporary permissions.
     $config = \Drupal::service('config.factory')->getEditable('migrate.settings');
     $config->set('temporary_permissions', []);
     $config->save();
     foreach ($permissions as $role => $role_permissions) {
       \Drupal::logger('migrate')->notice(
         // ...
       );
     }
     if ($permissions) {
       \Drupal::messenger()->addWarning(
         // something using array_keys($permissions)
       );
     }

    (The logger channel should be 'migrate', right?) See #92.4.

  6. I think we have to worry about the edge case where there is more than one migration that uses the entity:role destination. Maybe one migration creates roles and then another one updates them.

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,133 @@
     +  public function rollback(array $destination_identifier) {
     +    // Simply remove all the temporary permissions.
     +    if ($this->config->get('temporary_permissions')) {
     +      $this->config->set('temporary_permissions', []);
     +      $this->config->save();
     +    }
     +    parent::rollback($destination_identifier);
     +  }

    Suppose foo_migration adds the foo permission to one role and bar_migration adds the bar permission to another role. Then we roll back just one of the migrations. The migrate module still has to provide the other permission.

    I think we have to do something like this:

    1. parent::rollback(...)
    2. Get the temporary permissions.
    3. Get all assigned permissions by looping through all roles.
    4. Take the intersection.
    5. Set temporary permissions.

    Maybe there is a simpler way.

  7. I still think that we have to remove temporary migrations when another module that provides them is enabled. I guess this can be done in a follow-up issue if we want to limit the scope of this issue to migrations using the core UI. See #95.

quietone’s picture

Status: Needs work » Needs review
FileSize
5.9 KB
25.79 KB

@benjifisher, thanks for prompt reply. I didn't get to this till late so kept to simple things and, hopefully, having done anything daft.

1. todo
2. Typo fixed and gets a new $config object for testing.
3. Ah, yes missed this test item. I played around with this and found that the dependency on migrate.settings is set in the user role config. However, when a permission is removed from migrate.settings it is not updated.
4. Copy/paste error fixed.
5. If this change is made then we are looping through all the roles twice. Once in the new temporaryPermissionsByRole method and then again in the ConfigUpdater method. That seems inefficient. Also, should a method for cleaning the roles be put somewhere else and be available to custom code?
6. Or worse, if more than one migration adds unique temporary permissions to the same role there is no way to determine which permission was added by which migration. I don't see a way to handle all possibilities, We can only cleanup at the end. With that in mind I have removed all the rollback code.
7. todo

yogeshmpawar’s picture

Resolved CSpell error & added an interdiff.

benjifisher’s picture

@yogeshmpawar:

Thanks for removing the unused use statement.

@quietone:

Re #126.5: I was thinking that we would not need ConfigUpdater at all. Deleting permissions from the config object should have the effect of updating the roles and removing the temporary permissions.

According to #126.3, I am wrong about that.

I will do some manual testing, but I might not have time before today's migration meeting.

Status: Needs review » Needs work

The last submitted patch, 127: 2953111-127.patch, failed testing. View results

yogeshmpawar’s picture

Status: Needs work » Needs review
FileSize
26.14 KB
592 bytes

Updated patch will fix test failure.

Status: Needs review » Needs work

The last submitted patch, 130: 2953111-130.patch, failed testing. View results

yogeshmpawar’s picture

Status: Needs work » Needs review
FileSize
26.55 KB
732 bytes

one more try to fix test failures.

benjifisher’s picture

FileSize
1.45 KB

I sometimes forget about the "T" in RTBC. I did some manual testing, focusing on the config dependency.

The attached test_user_role.yml is a copy of d7_user_role.yml that uses the embedded_data source plugin to create two roles, with two permissions each.

The hard part was installing drush, thanks to #3264918: Update symfony/console to Symfony 6. See Remove DrushArgvInput (PR for drush).

Testing steps

  1. Install Drupal and enable the Migrate module: drush si standard and drush en migrate.
  2. Run the migration: drush mim test_user_role.
  3. Check messages: drush mmsg test_user_role. Results:
     ------- -------------- ------------------- --------------------------------- 
      Level   Source ID(s)   Destination ID(s)   Message                          
     ------- -------------- ------------------- --------------------------------- 
      2       Foo role       foo_role            Permission(s) 'delete any forum  
                                                 content, foo test permission'    
                                                 not found.                       
      2       Bar role       bar_role            Permission(s) 'edit any forum    
                                                 content, bar test permission'    
                                                 not found.                       
     ------- -------------- ------------------- --------------------------------- 
    
  4. Check the status report, /admin/reports/status. Find a warning with a link to ...
  5. Check /admin/people/permissions/module/migrate: roles and permissions are created as expected.
  6. Check dependencies. On /admin/config/development/configuration/single/export, select Configuration type: Role and one of the new roles. It has dependencies on the migrate module and the migrate.settings configuration.
  7. Check the Migrate settings. On the same page, select Configuration type: "Simple configuration" and migrate.settings. It lists the four temporary permissions, and each has a dependency on migrate.settings. It does not include the dependency on the module, but that is OK.
  8. Update the config entity, removing one of the permissions. Copy the YAML from the previous step, then paste it in to /admin/config/development/configuration/single/import. Submit the form.
  9. Return to the Migrate permissions page (Step 5). The deleted permission does not appear.
  10. Repeat Step 6. This is where the problem is. Even though the permissions do not show up on the Permissions form, they are still included in the role.
  11. Go back to the Migrate permissions page and submit the form. I get a WSOD. The log shows a PHP error: "RuntimeException: Adding non-existent permissions to a role is not allowed. The incorrect permissions are "foo test permission". in Drupal\user\Entity\Role->calculateDependencies() (line 206 of /var/www/html/core/modules/user/src/Entity/Role.php)."

We have a problem here. I will have to figure out what it takes to get the user roles to update whenever the Migrate settings change.

benjifisher’s picture

I realize that I have been making two mistakes regarding the config dependency system:

  1. Configuration (such as a user role) is updated when something it depends on is deleted, not on any update.
  2. Only config entities, not simple configuration objects, can be used as dependencies.

My hope that updating migrate.settings would trigger removing permissions from roles was wrong for both reasons. Updating migrate.settings is not enough because of (1). And migrate.settings is an object of type Drupal\Core\Config\ImmutableConfig, which does not implement Drupal\Core\Entity\EntityInterface. For example, that interface declares getConfigDependencyKey().

I often use "config object" and "config entity" interchangeably. That is a mistake.

I was trying to figure out why deleting the Article content type triggers an updated of the Content Editor role, removing related permissions, but removing migrate.settings did not do the same thing. The reason is (2).

I still think that this issue should take advantage of the config dependency system. That is more complicated than the already implemented system based on migrate.settings, so I have to give some good reasons.

As I said in #128, we should not need to use ConfigUpdater at all if we rely on the config dependency system. So we save a little complexity there.

To be clear, I am suggesting that the Migrate module define a config entity type, say Drupal\migrate\Entity\TemporaryPermission. Create one of these config entities for each temporary permission.

We decided not to support rollbacks in this issue. That is OK, since the scope of this issue is to support migrate_drupal_ui. But the solution we create here has to allow a follow-up issue to support rollbacks. I think the more granular structure (one config entity per temporary permission) will make that easier. We might want to add additional properties to the config entity in a follow-up issue, so that the entity saves the information we need to react to a rollback.

We already have one follow-up (child) issue to delete temporary permissions. We also have to delete temporary permissions when another module implements the permanent permission with the same name. That might be in this issue or in a follow-up. I think this is the real reason I want to make use of the config dependency system: there are several situations where we want to delete temporary permissions, and I do not want to use ConfigUpdater each time.

benjifisher’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update

I am setting the status to NW for #134 and for an issue summary update.

Now that this issue targets the 10.0.x branch, I think we should update the issue summary. (We should also update the title, as I said before.) I checked out the 10.0.x branch and repeated the first two testing steps from #133:

[error] Adding non-existent permissions to a role is not allowed. The incorrect permissions are "delete any forum content", "foo test permission". (/var/www/html/core/modules/user/src/Entity/Role.php:206)
[error] Adding non-existent permissions to a role is not allowed. The incorrect permissions are "edit any forum content", "bar test permission". (/var/www/html/core/modules/user/src/Entity/Role.php:206)
2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
[notice] Processed 2 items (0 created, 0 updated, 2 failed, 0 ignored) in 0.1 seconds (1242.6/min) - done with 'test_user_role'

We should mention this in the issue summary. I am pretty sure that we will get the same result if we try to migrate a permission depending on the Blog content type if that type has not yet been created. That is likely to happen with migrate_drupal_ui, so it is in scope for this issue.

quietone’s picture

Status: Needs work » Needs review

This converts the storage of temporary permissions from a config object to a config entity. I am not able to add a comment right now but it is worth seeing if this breaks anything.

quietone’s picture

Forgot to upload the patch.

Status: Needs review » Needs work

The last submitted patch, 137: 2953111-136.patch, failed testing. View results

quietone’s picture

Title: Only migrate role permissions that exist on the destination » Migrate non-existing permissions to temporary migrate permissions
Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs issue summary update +Bug Smash Initiative

The failing tests are not from any tests changed in the patch. There are failures because this patch adds a new configentity.

#133. On 10.0.x I manually tested using these steps, well sort of. Instead of the UI I used drush to delete 'foo test permission', the role was updated correctly. I did do a `drush mr test_user_role` and that caused a fatal on admin/people/permissions/module/migrate because the config entity was deleted. Adding as task for that to the remaining tasks.

#134. The implementation of the configentity was a learning experience so I may have not done things in the correct way.

Since we are not using the configupdated the batch doesn't know which roles were changed. Therefore, the displayed message now shows the permissions that were not migrated which gives the user something to work with.

The Upgrade tests now have to add an entity count for temporary_permissions, which will effect any contrib test that uses the base class. Should be a small number, most contrib don't write a functional test. At least, the last time I checked about a year ago.

Hopefully, the changes to the IS are accepted so removing tag. Title change as well. Adding Bug Smash tag.

benjifisher’s picture

Status: Needs review » Needs work

@quietone:

Thanks for the update!

I looked at the interdiff about a week ago, and it looks good. I owe you a closer look.

I did some testing, similar to the steps in #133. I ran into some problems. The numbering below does not correspond to the steps in #133.

  1. When I check dependencies (Step 6) I see that the new roles depend on the Migrate module, but they do not depend on the TemporaryPermission config entities. I think we have to add that dependency in the permissions callback. See the CR I mentioned in #92.
  2. It looks as though the config entities get the permission name as their ID. Can we translate spaces to underscores? I am not sure if this is a requirement for entity IDs, but I think it is expected. To inspect the config entity with Drush, I had to add quotation marks: drush cget 'migrate.temporary_permission.bar test permission'.
  3. As expected, the missing dependencies ((1) above) mean that the roles are not updated when the config entities are removed. If you look at the UI (/admin/people/permissions/module/migrate) you will not see them, but if you look at the role configuration, the phantom permission is still there. I still get a WSOD when I submit the form on the Migrate permissions page.
  4. Several comments ago, I asked what happens when two modules define the same permission. Looking at the code for getPermissions(), I think that part of the answer is that a static permission (from a .permissions.yml file) always "wins" over a dynamic permission (from a callback). If two modules define the same permission from callbacks, then I think it depends on the order (alphabetical?) of the modules. Based on this, I have to reverse the request I made earlier for the status report. A link to the Migrate permissions page is not good enough, since it might not show all the temporary permissions. Can we go back to listing them explicitly in the warning message?
benjifisher’s picture

I looked into the test failures. Searching on the error message led me to #3104189: entity type needs to be installed - Cache, Request Dispatcher, Request Handler, which refers to search_api_solr_update_8331(). That looks like a model we could follow here.

The change record Write update functions for entity schema updates, automation removed is relevant, but does not have any examples of adding new config entity types. The documentation page Creating a configuration entity type does not mention update functions. We should add a section to it once we get this working.

quietone’s picture

Status: Needs work » Needs review
FileSize
7.59 KB
28.16 KB

140.2 I meant to come back to fixing the names, thanks for the reminder. The only change in this patch is to add an 'id' field that is a machine name of the permission.

edit: fix grammar and typo.

Status: Needs review » Needs work

The last submitted patch, 142: 2953111-142.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review

The test failures are the same as in #137 plus a random failure, therefor setting to NR. I should be able to work on the other points tomorrow.

benjifisher’s picture

For comparison, here are the permissions defined by the Filter module after installing the Standard profile:

>>> $filter_permissions->permissions ()
=> [
     "use text format basic_html" => [
       "title" => Drupal\Core\StringTranslation\TranslatableMarkup {#4572},
       "description" => [
         "#prefix" => "<em>",
         "#markup" => Drupal\Core\StringTranslation\TranslatableMarkup {#4581},
         "#suffix" => "</em>",
       ],
       "dependencies" => [
         "config" => [
           "filter.format.basic_html",
         ],
       ],
     ],
     "use text format restricted_html" => [
       "title" => Drupal\Core\StringTranslation\TranslatableMarkup {#4578},
       "description" => [
         "#prefix" => "<em>",
         "#markup" => Drupal\Core\StringTranslation\TranslatableMarkup {#4579},
         "#suffix" => "</em>",
       ],
       "dependencies" => [
         "config" => [
           "filter.format.restricted_html",
         ],
       ],
     ],
     "use text format full_html" => [
       "title" => Drupal\Core\StringTranslation\TranslatableMarkup {#4585},
       "description" => [
         "#prefix" => "<em>",
         "#markup" => Drupal\Core\StringTranslation\TranslatableMarkup {#4584},
         "#suffix" => "</em>",
       ],
       "dependencies" => [
         "config" => [
           "filter.format.full_html",
         ],
       ],
     ],
   ]
quietone’s picture

Another increment. Yes, I too looked at the filter permissions for comparison, thanks.

#140-1 and 3. This patch is intended to fix the problem with the dependency. As suggested in #140-1, the dependency is added in the migrate permissions callback. With that in place I found that PermissionTest was wrong because it never actually saved the role when assigning a temporary permission. Oops! That is now fixed and also added a check that the intended checkbox it ticked on admin/people/permissions page for the temporary permission. Another change is that the schema use 'title' instead of 'permission' because the PermissionHandler sorts permissions on 'title'. That of course, required changes to several files. With a bonus that TemporaryPermission is just annotation.

Status: Needs review » Needs work

The last submitted patch, 146: 2953111-146.patch, failed testing. View results

quietone’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
17.41 KB
4.64 KB
30.61 KB

The failing tests are the same ones.

#140-4. This adds back the list of temporary permissions to the Status Page and thus some adjustments to the PermissionTest. And here is a screenshot of the status page with two temporary permissions.

Status: Needs review » Needs work

The last submitted patch, 148: 2953111-148.patch, failed testing. View results

benjifisher’s picture

I am having a quick look at the interdiff from #146.

     $all = [];
-    $temporary_permissions = \Drupal::entityTypeManager()->getStorage('temporary_permission')->loadMultiple();
+    $temporary_permissions = $this->storage->loadMultiple();
     foreach ($temporary_permissions as $temporary_permission) {
-      $all[] = $temporary_permission->getPermission();
+      $title = $temporary_permission->get('title');
+      $all[$title] = [
+        'title' => $title,
+        'dependencies' => [
+          'config' => ['migrate.temporary_permission.' . $temporary_permission->id()],
+        ],
+      ];
     }
  1. Is ->get('title') equivalent to ->label()?
  2. We could eliminate $temporary_permissions.
  3. This might be a little neater using array_map() instead of foreach(). Does loadMultiple() return an array with the right keys?

+1 for the updates to the test.

quietone’s picture

Status: Needs work » Needs review
FileSize
14.29 KB
42.22 KB

#150
1- Changed the temporary config entity definition to use 'label'.
2-3. Yes. This patch uses array_map instead of the foreach loops. The array returned by loadMultiple is keyed by the id (machine name) of the entity, but the PermissionHandler expects that to be the 'label'. For that foreach loop I added a function to build the array.

This patch also should take care of the test failure from Rest and JSON:API. That required adding a 'administer temporary permission' to migrate.permissions.yml and adding several test files. There were plenty of examples in core of the test files that were needed so there was a lot of copy/paste.

All going well, that leaves the update related fixes to do.

Status: Needs review » Needs work

The last submitted patch, 151: 2953111-151.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
4.14 KB
46.49 KB

And now a start on the update tests. I used hook_update_N. The hook starts by installing the new config entity. Then it will search all roles for permissions that are not valid. For those that are not valid a temporary_permission entity is created. This is assuming that all non-valid permissions are from migrate. Testing of the update is added as well. It adds the same two permissions to anonymous and administrator and checks that the new entities are created. This still feels a bit rough, but something is better than nothing.

Locally, the failing test passes so that is a good sign.

benjifisher’s picture

I did manual testing again. Things have changed since #133, so here are my current steps.

Testing steps

  1. Install Drupal and enable the Migrate module: drush si standard and drush en migrate.

  2. Run the migration: drush mim test_user_role.

  3. Check messages: drush mmsg test_user_role. Results:





    Level Source ID(s) Destination ID(s) Message
    2 Foo role foo_role Permission(s) ‘delete any forum content,
    foo test permission’ not found.
    2 Bar role bar_role Permission(s) ‘edit any forum content,
    bar test permission’ not found.
  4. Check the status report, /admin/reports/status. Find a warning with a list of the four temporary poermissions.

  5. Check /admin/people/permissions/module/migrate: roles and permissions are created as expected.

  6. Check dependencies. On /admin/config/development/configuration/single/export, select Configuration type: Role and one of the new roles. It has dependencies on the migrate module and the two configuration entities.

  7. Check the config entities. On the same page, select Configuration type: "Temporary permission" and "foo test permission". It does not have a dependency on the migrate module. What will happen when I uninstall the module?

  8. Delete one of the config entities: use drush php to get an interactive shell, then Drupal::entityTypeManager()->getStorage('temporary_permission')->load('foo_test_permission')->delete();

  9. Return to the Migrate permissions page (Step 5). The deleted permission does not appear.

  10. Repeat Step 6. The deleted config entity is not listed as a dependency, and the corresponding permision is gone.

  11. Go back to the Migrate permissions page and submit the form. I get a standard confirmation message.

  12. Uninstall and re-install the Migrate module: drush pmu migrate and drush en migrate. The config entities and related permissions are removed, and they are not re-created. The roles remain, without permissions nor dependencies.

My first attempt at (8) did not work: drush cdel migrate.temporary_permission.foo_test_permission. It looks as though this deletes the config object, not the config entity.

The question in (7) is answered in (12).

benjifisher’s picture

Status: Needs review » Needs work

@quietone:

Great work! Not only do you get the json_api and rest tests to pass, but you added more. I confess, I did not look too closely at the new tests.

The first two points are a little long, but all them fall into the category of clean-up:

  1. This will not give the correct link on a multilingual site, or if Drupal is installed in a subdirectory of the web root:

     +++ b/core/modules/migrate/migrate.install
     ...
     +      $description = new PluralTranslatableMarkup(
     +        count($temporary_permissions),
     +        t('An upgrade process created the following <a href=":temporary_permissions">temporary permission</a> that should be removed.',
     +          [':temporary_permissions' => '/admin/people/permissions/module/migrate']),
     +        t('An upgrade process created the following <a href=":temporary_permissions">temporary permissions</a> that should be removed.',
     +          [':temporary_permissions' => '/admin/people/permissions/module/migrate']),
     +      );

    If you need a link, use Url::fromRoute(). But I am not sure that a link is useful here. On the Migrate permissions form, we can add permissions to a role or remove them, but we cannot delete the permissions. Also, if another module creates the permission, then it might not appear at all on the Migrate page. We can come back to this in #3272352: Add a UI for deleting temporary permissions.

  2. What is the use case for this?

     +/**
     + * Enable temporary permissions.
     + */
     +function migrate_update_10001(&$sandbox) {
     ...
     +  // Search the existing roles for invalid permissions and create a
     +  // temporary_permission entity for each one.
     +  $roles = $entity_type_manager->getStorage('user_role')->loadMultiple();
     +  $valid_permissions = array_keys(\Drupal::service('user.permissions')->getPermissions());
     ...

    I guess this update function will run if the Migrate module is enabled in Drupal 9 and the site is upgraded to Drupal 10. So it might be a site under development (upgrading from D7 to D9-oh-no-it-has-to-be-D10). Or it might be a site that uses the Migrate API for continuing imports and also has some "phantom permissions" (migrated from D7 or left over from deleted config and uninstalled modules before Drupal 9.3).

    But we do not support upgrading from Drupal 9.2 to 10. That means user_post_update_update_roles() has already run. So most of those phantom permissions were already cleaned up. There still might be some created by migrations. Do we want to convert those to temporary permissions?

    We have already decided that the scope of this issue is to support UI migrations (or migrate_drupal_ui). I do not think we need to support D7-to-D9 in the UI, followed by upgrading to D10, followed by enabling modules and creating config that provide the temporary permissions. Besides, there is no way in the scope of this issue to delete the temporary permissions.

    Given the scope of this issue, I think we can simplify: remove this part of the function.

  3. I think we should make it plural: "administer temporary permissions". The description is optional, and I think we do not need it. Maybe, when we add a way to delete them, we can add a description.

     +++ b/core/modules/migrate/migrate.permissions.yml
     @@ -0,0 +1,7 @@
     +administer temporary permission:
     +  title: 'Administer temporary permission'
     +  description: 'tbd'
     +  restrict access: true

    In #151, you said that we need a permission to get the tests to pass. Does it have to be a permission defined by this module, or can it be an existing permission, like administer roles and permissions from the user module?

  4. That is not what you meant to write, is it? ;)

     +++ b/core/modules/migrate/src/MigratePermissions.php
     @@ -0,0 +1,77 @@
     ...
     +  /**
     +   * Provides temporary permissions for migrating temporary permissions.
     +   *
     +   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
     +   *   The storage for the temporary permissions.
     +   */
     +  public function __construct(EntityStorageInterface $storage) {
  5. Instead of hard-coding 'config' and 'migrate.temporary_permission.', I would follow the example of the other permissions callbacks and use [$temporary_permission->getConfigDependencyKey() => $temporary_permission->getConfigDependencyName()]. Or use BundlePermissionHandlerTrait::generatePermissions(). (See Drupal\Node\NodePermissions::nodeTypePermissions().)

     +  public function buildPermissions(array $temporary_permissions): array {
     +    return array_column(array_map(function ($temporary_permission) {
     +      $title = $temporary_permission->label();
     +      return [
     +        $title,
     +        [
     +          'title' => $title,
     +          'dependencies' => [
     +            'config' => ['migrate.temporary_permission.' . $temporary_permission->id()],
     +          ],
     +        ],
     +      ];
  6. Didn’t we decide that get('title') is the same as label()?

     +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php
     @@ -283,6 +283,26 @@ public static function finished($success, $results, $operations, $elapsed) {
     ...
     +    foreach ($temporary_permissions as $temporary_permission) {
     +      $list[] = $temporary_permission->get('title');
     +    }
  7. Suggestion: call the variable $id instead of $perm.

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,120 @@
     ...
     +      foreach ($does_not_exist as $permission) {
     +        $perm = preg_replace('/[^a-z0-9_]+/', '_', $permission);
     +        if (!$this->permissionStorage->load($perm)) {
     +          $this->permissionStorage
     +            ->create([
     +              'id' => $perm,
     +              'title' => $permission,
     +              'migration_id' => $this->migration->getPluginId(),
     +            ])
     +            ->save();
quietone’s picture

Status: Needs work » Needs review
FileSize
10.83 KB
41.9 KB

but all them fall into the category of clean-up:

Hooray!

1. Thanks for the info about Url::fromRoute(). I have removed the link but I wonder if we should add a link to a wiki page that provides more information. Such as uninstalling the migrate module will remove the temporary permissions or why it is a good idea to remove them.
2. It was a good idea at the time? I was off with the fairies? It is all removed. Yes, good point about scope.
3. Change the permission name to be plural and removed the description property.

I created the permission because of errors like this:

1) Drupal\Tests\migrate\Functional\Jsonapi\TemporaryPermissionTest::testGetIndividual
RuntimeException: Adding non-existent permissions to a role is not allowed. The incorrect permissions are "administer temporary permission".

which makes sense because the entity is defined with admin_permission = "administer temporary permission". (If this is kept maybe that should be plural.)

It can be changed to admin_permission = "administer permissions" and with corresponding changes in the json test, that works. However, it also means that migrate has a dependency on the user module for that permission. Is that really what we want to do?

4. Fixed.
5. Reworked to model off of NodePermission. Much nicer now.
6. Yes, we did and I missed that one. Fixed now.
7. Yes, that makes it easier to read.'

Let's see what the test bot thinks.

Status: Needs review » Needs work

The last submitted patch, 156: 2953111-155.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
571 bytes
42.24 KB

I forgot that the new config entity needs to be enabled in an update hook.

benjifisher’s picture

I am getting ready to leave for DrupalCon (early tomorrow morning) so I do not have time to look at the code, but I can reply to some points in #156.

It can be changed to admin_permission = "administer permissions" and with corresponding changes in the json test, that works. However, it also means that migrate has a dependency on the user module for that permission. Is that really what we want to do?

The user module is required, so I am not worried about adding a dependency on it.

As far as I can tell, the new permission is never used. Maybe, if we add CRUD routes to the TemporaryPermission annotations, the Entity system will use the permission declared in the annotations.

It looks as though migrate_drupal_ui has a custom access check for uid = 1 instead of relying on permissions.

It seems simpler to me if we do not introduce a new permission, but you can leave it in if you disagree. Either way, we can reconsider when we work on the follow-up issue #3272352: Add a UI for deleting temporary permissions.

I created the permission because of errors like this: ...

The test errors are a result of changes made in #2571235: [regression] Roles should depend on objects that are building the granted permissions. In the "Proposed resolution" section of that issue, I added this:

The changes in this issue require updates to several existing tests.

  • Only use permissions defined by modules. Fix typos. Do not use random strings.
benjifisher’s picture

Status: Needs review » Needs work

@quietone:

I reviewed the entire patch in #158. It looks really good, but I think there are some more things we can clean up.

  1. Can we use a more descriptive variable name, like $permission_names, instead of $all?

     +++ b/core/modules/migrate/migrate.install
     @@ -5,9 +5,72 @@
     ...
     +function migrate_requirements($phase) {
     ...
     +    $all = array_map(function ($temporary_permission) {
     +      return $temporary_permission->label();
     +    }, $temporary_permissions);
  2. Same request here:

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -166,6 +166,33 @@ public function testUserRole() {
     ...
     +    $all = array_values(array_map(function ($temporary_permission) {
     +      return $temporary_permission->label();
     +    }, $temporary_permissions));
     +    $this->assertSame($migrate_permissions, $all);
  3. Change “entity” to “entity type” in the comment. Now that we have removed the other part of this function, maybe we can consolidate the doc block and the in-line comment.

     +/**
     + * Enable temporary permissions.
     + */
     +function migrate_update_10001(&$sandbox) {
     +  // Install the new entity.
  4. We made the label plural, but not the machine name. Can we be consistent? (Maybe I have an agenda: it is just as much work to replace “administer temporary permission” with the plural as it is to replace it with “administer permissions”. Either way, you have to update here, the entity annotations, and the tests.)

     +++ b/core/modules/migrate/migrate.permissions.yml
     @@ -0,0 +1,6 @@
     +administer temporary permission:
     +  title: 'Administer temporary permissions'
     +  restrict access: true
  5. Comparing with NodeType, I think this should be “Temporary permissions” (sentence case and plural). This will be used as the title or heading when (if?) we add a page listing the temporary permissions. At least, I assume that is how this is used.

     +++ b/core/modules/migrate/src/Entity/TemporaryPermission.php
     @@ -0,0 +1,34 @@
     ...
     + *   label_collection = @Translation("temporary permission"),
  6. It is good and simple to have an entity class that is all annotation, as you pointed out in #146. But I think a static method, taking $permission and $migration (strings) and returning an array keyed by 'id', 'title', and 'migration_id', would make this patch more DRY. I see a lot of places where $storage->create() is called, and each one has the same preg_replace().

    If you can think of a better place to put the helper function, then we can keep this class as “all annotation”.

        +class TemporaryPermission extends ConfigEntityBase {}
  7. If we use TemporaryPermission::loadMultiple(), then the $storage property is never used. If we want to make the class easier to test, then we should keep the dependency injection and not hard-code the class name. Otherwise, we can remove this property, the constructor, the create() method, the interface, and the related use statements.

     +++ b/core/modules/migrate/src/MigratePermissions.php
     @@ -0,0 +1,80 @@
     +  /**
     +   * The storage for temporary permissions.
     +   *
     +   * @var \Drupal\Core\Entity\EntityStorageInterface
     +   */
     +  protected $storage;
  8. When you switched to using BundlePermissionHandlerTrait, you forgot to update this doc block. The parameter is a single entity, and the return type is string[][].

     +  /**
     +   * Build the permissions array.
     +   *
     +   * @param \Drupal\migrate\Entity\TemporaryPermission $temporary_permission
     +   *   An array of temporary permission config entities.
     +   *
     +   * @return string[]
     +   *   An associative array of permission names.
     +   */
     +  public function buildPermissions(TemporaryPermission $temporary_permission): array {
     +    $label = $temporary_permission->label();
     +    return [
     +      $label => [
     +        'title' => $this->t('%permission_name', ['%permission_name' => $label]),
     +      ],
     +    ];
     +  }
  9. (same snippet) I do not think we want to translate the permission names, since we are just using the machine names from the migration. That means we do not need StringTranslationTrait.

  10. The first code comment does not really apply to the line that follows, and it duplicates the next comment. Since $storage is used several times, I would like to make it easier to find: move it to the very top of the function. I do not think it needs a comment.

     +++ b/core/modules/migrate/tests/src/Functional/System/PermissionTest.php
     @@ -0,0 +1,102 @@
     ...
     +    // Create temporary permissions and confirm they are displayed.
     +    $storage = \Drupal::service('entity_type.manager')->getStorage('temporary_permission');
     +
     +    // Create a temporary permission.
     ...
  11. We can simplify this a bit: $storage->load('foo_permission')->delete();.

     ...
     +    $permission = $storage->load('foo_permission');
     +    $storage->delete([$permission]);
  12. It looks as though the code was refactored and we forgot to update the variable name:

     +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php
     @@ -283,6 +283,26 @@ public static function finished($success, $results, $operations, $elapsed) {
     ...
     +    $entity_type_manager = \Drupal::entityTypeManager()->getStorage('temporary_permission');
     +    $list = [];
     +    $temporary_permissions = $entity_type_manager->loadMultiple();
     +    foreach ($temporary_permissions as $temporary_permission) {
     +      $list[] = $temporary_permission->label();
     +    }
     +    if (!empty($list)) {
     +      $message = new PluralTranslatableMarkup(
     +        count($list),
     +        'The permission %permissions was not migrated. Check the logs for details.',
     +        'The permissions %permissions were not migrated. Check the logs for details.',
     +        ['%permissions' => implode(', ', $list)]
     +      );
     +      \Drupal::messenger()->addWarning($message);
     +
     +      $entity_type_manager->delete($temporary_permissions);
     +    }
  13. (same snippet) Can we test !empty($temporary_permissions) before even defining $list? At the same time, consider changing $list to $permission_names (cf. (1)) and using array_map().

  14. (same snippet) Currently, the destination plugin saves the temporary permissions as a migration message, not a log message. Either change “Check the logs” here, or save it as a log message in the destination plugin. (I am thinking both as a log message and a migration message, but maybe just a log message.)

  15. Would it make sense to move this method from MigrateUpgradeExecuteTestBase to MigrateUpgradeTestBase so that we can use @inheritdoc here?

     +++ b/core/modules/rdf/tests/src/Functional/Migrate/Upgrade7Test.php
     @@ -139,6 +140,16 @@ protected function getMissingPaths() {
     ...
     +  /**
     +   * Gets roles with temporary permissions removed.
     +   *
     +   * @return string
     +   *   A comma separated list of roles that have had permissions removed.
     +   */
     +  protected function getTemporaryPermissions() {
quietone’s picture

Status: Needs work » Needs review
FileSize
16.68 KB
42.94 KB

This patch intends to address all the points in #160. It was all pretty straightforward, just two items to comment on.
6. I took a slightly different approach here by using preCreate(). As I was making the changes for #160 it seemed easier to just remove the id from the various create methods and do it in TemporaryPermission. This entity is not one contrib or custom code should be working with so moving the ID there seems fine to me. Whether that is kept of not maybe we should mark TemporaryPermission as internal.
15. I do not want to move those methods. The file MigrateUpgradeExecuteTestBase is intended for use only by tests that execute the upgrade where as MigrateUpgradeTestBase handles ones that check the various Migrate UI forms and tasks done before a migration is run. It does not help that there are abstract function declarations in MigrateUpgradeTestBase (getEntityCounts) that only make sense when running a migration. Their presence clutters up several Functional tests and I would really like to move those to MigrateUpgradeExecuteTestBase. I can also be convinced to move them.

benjifisher’s picture

Status: Needs review » Needs work

@quietone:

Thanks for the quick update!

I will try to remember the distinction between the two testing base classes. I figured it would be easier to ask you than to figure it out myself.

I think this issue still needs another round of cleanup:

  1. I think we can just use string for the @return comment. I checked the PHP docs for preg_replace(): you do not get an array when all arguments are strings, and the only time you get null is if there is an error.

     +++ b/core/modules/migrate/src/Entity/TemporaryPermission.php
     @@ -0,0 +1,75 @@
     ...
     +   * @return array|string|string[]|null
     +   *   The ID.
     +   */
     +  public static function buildId($permission_name) {
     +    return preg_replace('/[^a-z0-9_]+/', '_', $permission_name);
     +  }
  2. Same file: any new functions should declare a return type.

  3. You suggested making this class final, but did not do that. For now, someone can extend the class and change the buildId() method. To support that, I think we should call static::buildId() instead of self::buildId().

     ...
     +  public static function preCreate(EntityStorageInterface $storage, array &$values) {
     +    if (!isset($values['id'])) {
     +      $values['id'] = self::buildId($values['title']);
     +    }
     +  }
  4. If we need this method, then I think it should be a member of the storage class, not the entity class. For one thing, that is where load() and loadByProperties() are defined. For another thing, using Drupal::entityTypeManager()->getStorage() instead of dependency injection makes this method hard to test.

    This method is only called once, in EntityUserRole. It is easy to change that call to use buildId() instead. The simplest thing thing is to do that and remove this method.

     +  public static function loadByName($permission_name) {
     +    return \Drupal::entityTypeManager()
     +      ->getStorage('temporary_permission')
     +      ->load(self::buildId($permission_name));
     +  }
  5. Follow-up to #160.9: remove the two use statements for StringTranslation.

     +++ b/core/modules/migrate/src/MigratePermissions.php
     @@ -0,0 +1,51 @@
     ...
     +class MigratePermissions {
     +
     +  use BundlePermissionHandlerTrait;
     +  use StringTranslationTrait;
  6. In other places, you removed the $permission variable. We can do the same thing here.

     +++ b/core/modules/migrate/tests/src/Functional/System/PermissionTest.php
     @@ -0,0 +1,97 @@
     ...
     +  public function testStatusPage() {
     ...
     +    $permission = 'foo permission';
     +    $storage->create([
     +      'title' => $permission,
     +      'migration_id' => 'user_role',
     +    ])->save();
     ...
     +    $permission = 'bar permission';
     +    $storage->create([
     +      'title' => $permission,
     +      'migration_id' => 'user_role',
     +    ])->save();
  7. Follow-up to #160.11: just delete().

     +    $storage->load('foo_permission')->delete([$permission]);
  8. Follow-up to #160.13: If $temporary_permissions is not empty, then $permission_names should not be empty, so the second test is redundant.

     +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php
     @@ -6,6 +6,7 @@
     ...
     +    $temporary_permissions = $entity_type_manager->loadMultiple();
     +    if (!empty($temporary_permissions)) {
     +      $permission_names = array_map(function ($id, TemporaryPermission $temporary_permission) {
     +        return $temporary_permission->label();
     +      }, array_keys($temporary_permissions), array_values($temporary_permissions));
     +      if (!empty($permission_names)) {
  9. We want a logger, not the messenger service.

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,129 @@
     ...
     +  /**
     +   * The migrate message service.
     +   *
     +   * @var \Drupal\migrate\MigrateMessageInterface
     +   */
     +  protected $message;
quietone’s picture

Status: Needs work » Needs review
FileSize
13.5 KB
42.45 KB

At least the trend is going down, it was 15 points of cleanup and now it is 9.

1. Ah that was phpstorm and I didn't check it.
2. I've added return types to all new functions per https://www.drupal.org/docs/develop/standards/coding-standards#s-paramet....
3. I choose not make a change earlier so I could ponder some more and hear your thoughts. I can't think of a use case where someone should be mucking around with the temporary permissions. Is there? This is still todo.
4. I thought that as well, until I started looking into it and the I found that loadByName exists on FieldCong and FieldStorageConfig. But, it is removed anyway.
5. Done.
6. Done.
7. Done.
8. Oops, Fixed.
9. Why? The migrateMessenger logs to the 'migrate' channel.

quietone’s picture

I missed that the check failed. This removes a return type declaration.

benjifisher’s picture

@quietone:

Thanks for all the fixes. I reviewed 1, 2, 4, 5, 6, 7, and 8, and they all look good.

Re (3): Should the TemporaryPermissions class be final?

By default, I do not make classes final. I may not anticipate how the class might be extended and used, but that does not mean that it will not be extended. Maybe some other module will want to create config-based permissions, and they may want to extend this class. Why require them to copy it instead? Maybe the module needs to use a different set of allowed characters, or create the id based on something other than the title.

Do you have any other reason for declaring it final?

Re (9):

The migrateMessenger logs to the 'migrate' channel.

I did not know that. Thanks for pointing it out!

The code from this patch is

      $this->message->display($message, MigrationInterface::MESSAGE_WARNING);

and that constant is 2. Looking at Drupal\migrate\MigrateMessage, I see

  protected $map = [
    'status' => RfcLogLevel::INFO,
    'error' => RfcLogLevel::ERROR,
  ];

  /**
   * {@inheritdoc}
   */
  public function display($message, $type = 'status') {
    $type = $this->map[$type] ?? RfcLogLevel::NOTICE;
    \Drupal::logger('migrate')->log($type, $message);
  }

So it looks to me as though the current code will create a Notice, and that we could make it Info or Error. But we want a Warning, and we cannot get that with MigrateMessage.

Even though I did not understand MigrateMessage in my first comment, I still think we should use a logger.

I guess I will have to do some manual testing to see the log messages.

quietone’s picture

FileSize
663 bytes
42.44 KB

Can we just make this one line change instead of injecting the logger in to the destination plugin?

benjifisher’s picture

Status: Needs review » Needs work

I see a couple of problems with this:

   protected $map = [
     'status' => RfcLogLevel::INFO,
     'error' => RfcLogLevel::ERROR,
+    MigrationInterface::MESSAGE_WARNING => RfcLogLevel::WARNING,
   ];

For consistency with the other keys, I would use 'warning' instead of MigrationInterface::MESSAGE_WARNING.

The more serious problem is that this is not BC. Previously, any call to MigrateMessage::display() that uses the new key would log a Notice. Now it will log a Warning.

It looks to me as though you are using the MigrateMessage class for something it was not designed to do.

quietone’s picture

Status: Needs work » Needs review
FileSize
3.16 KB
42.72 KB

One injected logger coming right up!

benjifisher’s picture

@quietone:

I am going to do some manual testing now, starting with a D7 database that has some "phantom permissions".

Are you still considering whether to make the TemporaryPermissions class final?

benjifisher’s picture

Manual testing - Drupal 7 upgrade in the admin UI

I tried the following test, both with and without the patch in #168. When testing without the patch, I deleted the following lines from d7_user_role.yml:

  # A special flag so we can migrate permissions that do not exist yet.
  # @todo Remove in https://www.drupal.org/project/drupal/issues/2953111.
  skip_missing_permission_deprecation:
    plugin: default_value
    default_value: true
  1. Install Drupal 7 with the Standard profile.
  2. Add a test content type.
  3. Add related permissions to the Authenticated role.
  4. Delete the content type.
  5. Install Drupal 9 separately, with the Standard profile, and enable migrate_drupal_ui along with its requirements.
  6. Perform the upgrade, starting at /upgrade.

Without the patch, I got error messages in the Drupal log from the migrate_drupal_ui channel:

Adding non-existent permissions to a role is not allowed. The incorrect permissions are "access dashboard", "access overlay", "administer actions", "administer fields", "block IP addresses", "delete revisions", "delete terms in 1", "edit terms in 1", "revert revisions", "view revisions". (/var/www/html/core/modules/user/src/Entity/Role.php:206)

Curiously, I got two copies of each message for each affected role (Authenticated and Administrator). One copy is prefixed with "Source ID 2:" or "Source ID 3:". Maybe, somewhere in the code, we log an error message and then (re)throw an exception.

In the Drupal 9 Standard profile, the Administrator role automatically gets all permissions, so it is not assigned any individual permissions. Ideally, we would detect this in the migration and not add individual permissions to the admin role. (That is not in scope for this issue. I have not checked: we may already have an issue for that.)

With the patch, I get two warnings in the migrate channel. One is

Permission(s) 'create test content, delete any test content, delete own test content, edit any test content, edit own test content' not found.

I also get two errors (with the "Source ID #:" prefix). I also get a status message (warning) in the admin UI:

The permissions access dashboard, access overlay, administer actions, administer fields, block IP addresses, create test content, delete any test content, delete own test content, delete revisions, delete terms in 1, edit any test content, edit own test content, edit terms in 1, revert revisions, view revisions were not migrated. Check the logs for details.

The warning messages (logs and UI) are expected. :+1:

Why do we still get the error messages? My first guess is that we have to clear the entity cache. I am not sure whether we should do this after defining a temporary permission or when getting them (with TemporaryPermission::loadMultiple().) If we do it when creating the temporary permission, is it worth doing it once at the end of the loop (if any temporary permissions have been created)?

One other minor point: the destination plugin is defined in the user module, so maybe we should use that logger channel.

Summary:

  1. Avoid error messages when using migrate_drupal_ui.
  2. Consider changing the logger channel.
benjifisher’s picture

Status: Needs review » Needs work
benjifisher’s picture

P.S. I forgot to check the migrate messages. It works as expected:

 ------- -------------- ------------------- ----------------------------------- 
  Level   Source ID(s)   Destination ID(s)   Message                            
 ------- -------------- ------------------- ----------------------------------- 
  2       3              administrator       Permission(s) 'access dashboard,   
                                             access overlay, administer         
                                             actions, administer fields, block  
                                             IP addresses, delete revisions,    
                                             delete terms in 1, edit terms in   
                                             1, revert revisions, view          
                                             revisions' not found.              
  2       2              authenticated       Permission(s) 'create test         
                                             content, delete any test content,  
                                             delete own test content, edit any  
                                             test content, edit own test        
                                             content' not found.                
 ------- -------------- ------------------- ----------------------------------- 
quietone’s picture

Status: Needs work » Needs review
FileSize
1.07 KB
42.63 KB

Hmm, this is due MigrateMessage which assumes that messages are only 'status' and 'error'. I think that it too restrictive and should be changed. But then again this is the first time we have run into needing a 'warning' so maybe it's not an issue.

Anyway, I changed the destination to only use the logger, now with the 'user' channel. This now means there will be no message in the migrate_message table. The message is changed because I think it is helpful to add something from the source row, and I chose the role name for that. The message is now like.

The role 'anonymous user' has the following permission(s) that do not exit, 'migrate test anonymous permission.

I wasn't really happy with the 'migrate' channel either, I used it because it is a destination plugin. But that wasn't right either from a Migrate UI point of view, where all the messages are logged to 'migrate_drupal_ui'. When /upgrade is complete the final message includes a link (to route migrate_drupal_ui.log) to filtered logs. But now the permission errors are not there anymore. How is the user to find them?

As for using @final, I am not able to make a good argument so will follow your lead for now.

quietone’s picture

I think this will do what we want. The display message is a warning, the migrate_message is a \Drupal\migrate\Plugin\MigrationInterface::MESSAGE_WARNING and the watchdog message is a \Drupal\Core\Logger\RfcLogLevel::WARNING.

benjifisher’s picture

Status: Needs review » Needs work
Issue tags: +Needs followup

@quietone:

I re-tested with the patch in #174, and it works as expected. Nice job! I am glad that the fix is simpler than my first guess (in #170) of clearing the entity cache.

The only part I do not like is this hunk:

+++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php
@@ -212,7 +213,13 @@ public static function run($initial_ids, $config, &$context) {
       // Add and log any captured messages.
       foreach (static::$messages->getMessages() as $message) {
         $context['sandbox']['messages'][] = (string) $message;
-        \Drupal::logger('migrate_drupal_ui')->error($message);
+        // Log all messages as errors except Temporary permission messages.
+        if (preg_match('/Permission\(s\).*not found\./', end($context['sandbox']['messages']))) {
+          \Drupal::logger('migrate_drupal_ui')->warning($message);
+        }
+        else {
+          \Drupal::logger('migrate_drupal_ui')->error($message);
+        }
       }

That seems like too much of a special case: the batch processor should not "know" any details about the messages it is given.

I suggest that we revert that one change, accept error messages for now, and have a follow-up issue to update the batch processor (and the MigrateMessageCapture class) so that it pays attention to the $type of the message. I am adding the "Needs followup" tag for that.

One other thing: since we agreed not to declare the class final, make the change suggested in #162.3.

Summary:

  1. Revert the change shown above.
  2. Use static::buildId() instead of self::buildId().
quietone’s picture

Status: Needs work » Needs review
Issue tags: -Needs followup
FileSize
1.69 KB
42.16 KB

Yea, I wasn't happy with it either but it did what we wanted.

#175
1. Revert done.
2. change made

Follow up made, #3278115: Make the migrate messages aware of the error level so removing tag. I think it could use a better title.

Status: Needs review » Needs work

The last submitted patch, 176: 2953111-176.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review

The failure is a know random, Drupal\Tests\layout_builder\FunctionalJavascript\BlockFormMessagesTest::testValidationMessage, #3055982: Remove resizing window in BlockFormMessagesTest::testValidationMessage.

Setting to NR.

benjifisher’s picture

Status: Needs review » Reviewed & tested by the community

The failing test is mentioned in #3268070: Temporarily skip even more failing tests. I agree that we can ignore the failure here.

I compared the patch in #176 to the one in #168, which I tested. The differences are small, so I did not re-test.

Thanks for all your work on this issue, @quietone! I think the patch in #175 is ready to go.

quietone’s picture

Happy May Day to us!

benjifisher’s picture

Virtual high five!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 176: 2953111-176.patch, failed testing. View results

benjifisher’s picture

Status: Needs work » Reviewed & tested by the community

It looks like another random test failure, this time CKEditor5IntegrationTest::testArticleNode(). I ran the test locally, and it passed, so I am moving this issue back to RTBC.

alexpott’s picture

A couple of initial thoughts. I've not done a full review yet but somethings spring to mind.

  1. +++ b/core/modules/migrate/src/Entity/TemporaryPermission.php
    @@ -0,0 +1,59 @@
    +class TemporaryPermission extends ConfigEntityBase {
    

    I wonder if rather than a configuration entity we'd be better to use some simple configuration or state. Maintaining a configuration entity and all the Rest and Json stuff feels quite OTT. Or maybe you can use the * internal = TRUE, in the annotation and avoid all of this.

  2. +++ b/core/modules/user/src/Entity/Role.php
    @@ -202,7 +202,7 @@ public function calculateDependencies() {
    -    if (!empty($invalid_permissions) && !$this->get('skip_missing_permission_deprecation')) {
    +    if (!empty($invalid_permissions)) {
    

    Very cool to see this go.

quietone’s picture

#184.1 An earlier version of this patch did use a config object and not a config entity. That was changed to a config entity in order to take advantage of the config dependency system and also not have to use ConfigUpdated. Benjifisher explains this in more detail in #134.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Okay then the configuration entity should be marked as internal. There's no way this should ever be exposed via JsonAPI or Rest.

I do feel like one config entity per temporary permission feels quite excessive and the whole idea of deploying and managing temporary permissions using config feels a bit befuddling. Part of me feels like we should be using the state system here. Like you do the migrations - get informed the system is maintaining these temporary permissions and you either have to enable the modules that provide them or lose them when you do a deployment. I'm going to ask for more opinion from other committers on this.

larowlan’s picture

Discussed this with @catch and @alexpott - and we were of the opinion that it should be ok to just drop these permissions.
People can always easily re-enable them if they chose to reinstall the module at a later date.

Failing that, can we use state to store these temporary permissions instead of a config entity?

quietone’s picture

Status: Needs work » Needs review
Issue tags: +Needs issue summary update, +Needs change record updates
FileSize
28.22 KB
27.4 KB

Sure state can be used, config was initially used because of #71.

This patch uses state to store the temporary permissions. After an upgrade from /upgrade, the user is given a message listing all the temporary permissions, then they are removed from the roles and deleted.

The IS and change record need to be updated, adding tags.

quietone’s picture

Here is a version that simply drops the non existing permissions, as was done back around #15.

Status: Needs review » Needs work

The last submitted patch, 189: 2953111-189.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
FileSize
1.73 KB
17.35 KB

Oh, the d6 source plugin is sorted and the d7 one is not. Adjusting the D7 test accordingly.

benjifisher’s picture

Title: Migrate non-existing permissions to temporary migrate permissions » Only migrate role permissions that exist on the destination
Assigned: Unassigned » benjifisher

I am changing the title back to what it was before #139 and assigning this issue to myself for review. If it is still assigned to me after July 4, then feel free to reassign it.

Since we are going back to an earlier approach, I think an interdiff with one of the earlier patches would be more helpful than the one attached to #189. If I can figure out which of the earlier patches is closest, then I will attach an interdiff.

It looks as though the patch in #55 was RTBC before we postponed this issue on #2571235: [regression] Roles should depend on objects that are building the granted permissions. Can we use that as a starting point instead of #15? One change we made going from #15 to #55 was adding optional dependencies: see the last part of Comment #41, for example. I think that was a good idea. It means that we do not drop permissions that depend on configuration that is created as part of the migration.

benjifisher’s picture

Assigned: benjifisher » Unassigned
Status: Needs review » Needs work

Here are a few quick questions about the tests.

  1. Why remove the test for the destination IDs?

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -40,9 +42,7 @@ protected function assertRole(string $id, array $permissions, int $lookupId, Mig
          /** @var \Drupal\user\RoleInterface $role */
          $role = Role::load($id);
          $this->assertInstanceOf(RoleInterface::class, $role);
     -    sort($permissions);
          $this->assertSame($permissions, $role->getPermissions());
     -    $this->assertSame([[$id]], $id_map->lookupDestinationIds(['rid' => $lookupId]));
        }
  2. Why remove the type declarations?

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php
     @@ -31,34 +31,46 @@ protected function setUp(): void {
     ...
     -  protected function assertEntity(string $id, string $label, int $original_rid): void {
     +  protected function assertEntity($id, $label, $original_rid, array $permissions) {
quietone’s picture

Status: Needs work » Needs review
FileSize
2.18 KB
17.8 KB

@benjifisher, thanks for the review.
193
1. It was an oversight and I have restored the assertion. (I don't think the assertion has much value and it potentially confusing as the D7 assertRole method does not have the same assertion. It is a long standing dream to have the same entity assertion methods for all d6/d7 migration tests. But that ship has sailed).
2. Another oversight.

192. The patch in #55 adds optional dependencies to d6_user_role and d7_user_role as well as the 'validate_permissions' configuration key to the destination plugin. I am running out of steam so the patch here adds the optional dependencies but does not modify the tests or re-introduce the validate_permissions key.

Status: Needs review » Needs work

The last submitted patch, 194: 2953111-194.patch, failed testing. View results

benjifisher’s picture

Assigned: Unassigned » benjifisher
Status: Needs work » Needs review

It looks as though the testbot set this issue to NW. Someone (maybe me) queued a re-test. The second test passed, so I am setting the status back to NR.

I am assigning this issue to myself for the review.

benjifisher’s picture

Assigned: benjifisher » Unassigned
Status: Needs review » Needs work

I compared the patches in #55, #194.

  • The patch in #55 used the simpler pattern for dependency injection. Since @alexpott objected to that, we should use the usual form as in #194.
  • There are different choices for variable names in the destination plugin. I like $does_not_exist from #194 and $new_permissions from #55. Maybe $invalid_permissions and $valid_permissions would be even better.
  • In #55, we provided the option 'validate_permissions'. If not set, then we would migrate invalid permissions. Now that we are targeting 10.0, I do not think we should make that optional. See #135: we get an error when saving the role if we have added invalid permissions. In other words, it is a feature, not a bug, that the patch in #194 leaves out that option.

I reviewed the patch in #194. It looks really good! I just have a few questions and suggestions for polishing it.

  1. Do we have to provide a BC layer when we remove this option, or are we just following through on the deprecation we already made? I hope we can just remove it!

     +++ b/core/modules/user/src/Entity/Role.php
     @@ -202,7 +202,7 @@ public function calculateDependencies() {
          $permission_definitions = \Drupal::service('user.permissions')->getPermissions();
          $valid_permissions = array_intersect($this->permissions, array_keys($permission_definitions));
          $invalid_permissions = array_diff($this->permissions, $valid_permissions);
     -    if (!empty($invalid_permissions) && !$this->get('skip_missing_permission_deprecation')) {
     +    if (!empty($invalid_permissions)) {
            throw new \RuntimeException('Adding non-existent permissions to a role is not allowed. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '".');
          }
  2. It is inconsistent to apply array_keys() in the constructor for one property and in the create() method for another property. I think I like doing it in the create() method: if we want to test this class with an explicit list of permissions, then we can just pass in the array of permissions instead of mocking the permissions handler.

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,96 @@
     ...
     +  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, PermissionHandler $permission_handler) {
     +    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory);
     +    $this->destinationPermissions = array_keys($permission_handler->getPermissions());
     +  }
     +
     +  /**
     +   * {@inheritdoc}
     +   */
     +  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
     +    $entity_type_id = static::getEntityTypeId($plugin_id);
     +    return new static(
     +      $configuration,
     +      $plugin_id,
     +      $plugin_definition,
     +      $migration,
     +      $container->get('entity_type.manager')->getStorage($entity_type_id),
     +      array_keys($container->get('entity_type.bundle.info')
     +        ->getBundleInfo($entity_type_id)),
     +      $container->get('language_manager'),
     +      $container->get('config.factory'),
     +      $container->get('user.permissions'),
     +    );
     +  }
  3. (same snippet) Thanks for breaking the long line in the create() method. I see that it is a single line in the parent class. I think we should either keep it on one line, for consistency with the parent, or put the closing ), on a new line.

  4. Unless you feel strongly, can we use multi-line syntax for all of these arrays?

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -134,6 +115,42 @@ public function testUserRole() {
     ...
     +    // Permissions for each expected migrate message.
     +    $permissions[0] = ['migrate test anonymous permission'];
     +    $permissions[1] = [
     +      'access comments',
     +      'post comments',
     +      'skip comment approval',
     +      'migrate test authenticated permission',
     +    ];
     +    $permissions[2] = ['migrate test role 1 test permission'];
     +    $permissions[3] = [
     ...
  5. It would be simpler if we could use something like foreach ($messages as $count => $message), but the return type of getMessages() is \Traversable, so I guess we cannot rely on they keys like that.

     @@ -134,6 +115,42 @@ public function testUserRole() {
     ...
     +    $messages = $id_map->getMessages();
     +    $count = 0;
     +    foreach ($messages as $message) {
     ...
     +      $count++;
     +    }
  6. The parameter $original_rid is now unused. We do not have to deprecate it for BC, do we? Oh, good: this method is explicitly marked @internal.

    I think we will have to remove it from the doc block, the function declaration, and all calls to the function.

    The test is much better with the explicit lists of permissions.

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php
     @@ -31,34 +31,46 @@ protected function setUp(): void {
         *   The role's expected label.
         * @param int $original_rid
         *   The original (integer) ID of the role, to check permissions.
     +   * @param string[] $permissions
     +   *   The expected permissions.
         *
         * @internal
         */
     -  protected function assertEntity(string $id, string $label, int $original_rid): void {
     +  protected function assertEntity(string $id, string $label, int $original_rid, array $permissions): void {
     ...
     -    if (isset($original_rid)) {
     -      $permissions = Database::getConnection('default', 'migrate')
     -        ->select('role_permission', 'rp')
     -        ->fields('rp', ['permission'])
     -        ->condition('rid', $original_rid)
     -        ->execute()
     -        ->fetchCol();
     -      sort($permissions);
     -      $this->assertSame($permissions, $entity->getPermissions());
     -    }
     +    $this->assertSame($permissions, $entity->getPermissions());

We should think about additional test coverage. I guess the kernel test implicitly tests at least two things:

  • the effect of the optional dependencies
  • the destination plugin

Ideally, we would have some more fine-grained tests of these. Maybe a unit test for the destination plugin. Is it worth adding a kernel test to compare the result of the role migration with and without an optional dependency?

benjifisher’s picture

I reviewed some of the older comments.

  1. #71. I had forgotten that @alexpott first suggested that we create temporary permissions. It was an interesting idea!
  2. #84. @mikelutz pointed out that the core role migrations cannot add (optional) dependencies on contrib migrations. We can do that with hook_migration_plugins_alter(), can't we? Or does that run too late, after dependencies have been processed? I would add the tag for a CR update, but it is already there.

The more recent comments (starting with #187) suggest that it is OK to lose some migrations, if it saves a lot of complexity. I think it is worth adding optional dependencies for the core migrations. That is not a lot of complexity, and we reduce the number of permissions that have to be added back.

In fact, we might even have a follow-up issue to add optional dependencies for some common contrib modules. The precedent is all the custom code we have to support text filters from contrib modules.

quietone’s picture

Status: Needs work » Needs review
FileSize
18.5 KB
25.48 KB

It looks really good!

Music to my ears! :-)

197. I've updated the variable names in the destination plugin

197
1. I'll think about this later.
2. Done
3. Now the same as the parent.
4 and 5. These points are likely no longer relevant because the test now uses a data provider to allow for a test with the minimum of modules and a test with all optional dependencies. Adding the optional dependencies is the way to go so that when running from /upgrade the dependencies will run first.
6. Todo. I'll probably convert this to using a data provider as well.

I don't recall writing a unit test for a destination plugin. On searching I see that the only destination plugins that have unit tests are in the migrate module. Based on that I am thinking that the kernel tests will be sufficient.

hook_migration_plugins_alter() can be used to alter the dependencies of a migration. It is done in Commerce Migrate, commerce_migrate_commerce.module.

benjifisher’s picture

Issue summary: View changes

I do not have time now to review the latest changes.

Maybe it is worth adding a test based on the results I reported in #135 (using the test migration from #133). A test-only patch would support the Critical status of this issue.

Thanks for confirming that hook_migration_plugins_alter() can be used as I suggested in #198. I am adding a note to the "Remaining tasks" in the issue summary.

benjifisher’s picture

Status: Needs review » Needs work

The patch in #199 addresses most of the points in #197 and #198, Maybe #184.2 already answers #197.1. I added a "remaining task" to the IS for #198.2. I think that about covers it.

The new patch made a lot of changes to the tests, and I have several minor changes and suggestions for that:

  1. From the way it is used, $expected_permissions is string[][]. The outer keys are destination IDs (?) and the inner keys are 'valid' (?) and 'invalid'.

     +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php
     @@ -19,7 +20,40 @@ class MigrateUserRoleTest extends MigrateDrupal6TestBase {
     ...
     +  /**
     +   * Assert the logged migrate messages.
     +   *
     +   * @param string[] $expected_permissions
     +   *   An array of log messages.
     +   * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
     +   *   The migration ID map plugin.
     +   */
     +  public function assertMessages(array $expected_permissions, MigrateIdMapInterface $id_map) {
     +    foreach ($id_map->getMessages() as $message) {
     +      $permissions = implode(', ', $expected_permissions[$message->dest_id]['invalid']);
  2. Can you change the key 'roles' in the data provider to match the parameter name $permissions? Or maybe change both to something like role_data, since both roles and permissions suggests a flat list of strings.

     +  public function testUserRole(array $modules, array $migrations, array $permissions) {
     +    if ($modules) {
     +      // Install modules that have migrations that may cause permissions
     +      // to be created.
     +      $module_installer = \Drupal::service('module_installer');
     +      $module_installer->install([
     +        'block',
     +        'block_content',
     +        'comment',
     +        'contact',
     +        'config_translation',
     +        'language',
     +        'link',
     +        'menu_ui',
     +        'node',
     +        'taxonomy',
     +        'text',
     +      ]);
     +      $this->installEntitySchema('block_content');
     +      $this->installConfig(['block_content', 'comment']);
  3. (same snippet) Suggestion: shorten the comment to "… that may provide permissions."

  4. (same snippet): We should use the $modules parameter. Optionally, we can eliminate the variable $module_installer:

     \Drupal::service('module_installer')->install($modules);

    Alternatively, make the parameter a boolean. It seems odd to list the modules in the data provider and then install related configuration in this function.

  5. (same snippet) Document the parameters in the doc block. I am not sure that this is standard practice for test methods, but it seems like a good idea to me.

  6. I think this duplicates an assertion earlier in the test. Do we want to add migrate_test_role_41 to the helper function?

          // Test there are no duplicated roles.
     -    $roles[] = 'migrate_test_role_41';
     -    $this->assertEmpty(Role::loadMultiple($roles));
     +    $this->assertNoDuplicateRoles();
quietone’s picture

This patch should address all the items in #201. And good catch on point 6.

Shall I make the same changes to the Drupal 7 test? That is, test with the current dependencies and then will all of them?

Status: Needs review » Needs work

The last submitted patch, 202: 2953111-202.patch, failed testing. View results

benjifisher’s picture

That is not a random test failure, so the issue really NW.

Shall I make the same changes to the Drupal 7 test? That is, test with the current dependencies and then will all of them?

The default answer to "should I add test coverage?" is "of course", but maybe this is an exception. What we are testing here is how the upgrade handles dependencies, which should be independent of the sources (D6 or D7). Furthermore, I think we have agreed to keep the D6 source plugins until we are ready to move the D7 sources to contrib, so we will not be in a situation where we are removing test coverage.

In short, I think we can leave the D7 tests as is.

quietone’s picture

Status: Needs work » Needs review

The failures are due to #3082211: Migrate UI upgrade tests should provide the complete log, which has been reverted. I am restarting the tests.

quietone’s picture

I figured out the failure in sqlite by editing the results to place each permission on a new line.
Expected

 translate user-defined strings,
 use advanced search,
 use PHP for settings,
 use text format custom_text_format,

Actual

 translate user-defined strings,
 use PHP for settings,
 use advanced search,
 use text format custom_text_format,

This is the list of invalid permissions that is included in the migrate message and then later tested. Let's fix this by sorting the permissions before they are added to the message. And in the test make sure the expected_permissions array is sorted.

quietone’s picture

Oops, redoing the patch because I forgot to update the d6 test.

benjifisher’s picture

In #201.6, I was thinking that assertNoDuplicateRoles() duplicates the assertion assertEmpty(). I missed that there was executeMigration() in between. Is there any reason not to use assertNoDuplicateRoles() twice?

        $roles = [
          'anonymous1',
          'authenticated1',
          'administrator1',
          'migrate_test_role_11',
          'migrate_test_role_21',
          'migrate_test_role_3_that_is_longer_than_thirty_two_characters1',
        ];
        $this->assertEmpty(Role::loadMultiple($roles));

        // ...

        $this->executeMigration('d6_user_role');

        // Test there are no duplicated roles.
        $this->assertNoDuplicateRoles();

Other than that, the patch in #207 addresses all the points in #201.

quietone’s picture

Not really, we are testing more possible duplicates than we need to in each case but I do like that it makes the test a wee bit easier to read. The only reason I initially didn't use it twice was to not introduce another change to the test.

benjifisher’s picture

@quietone:

Since this issue introduces the helper function, I think it is in scope to use it to simplify the test. But I will defer to you: if you want to avoid further changes, then that is OK.

I am going to work on the CR now. Before RTBC, we should handle the "Remaining tasks" in the issue summary. I think some of those are obsolete, since we switched from the "temporary permissions" approach back to the "remove invalid permissions" approach.

quietone’s picture

Issue summary: View changes

Just a few changes to the Issue Summary.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Needs work
Issue tags: -Needs issue summary update, -Needs change record updates

I did some manual testing and updated the issue summary and the
change record. I noticed a couple of problems with the feedback.

  1. When testing manually (see #133, #135) I get the following
    migration messages:

     ------- -------------- ------------------- ----------------------------------- 
      Level   Source ID(s)   Destination ID(s)   Message                            
     ------- -------------- ------------------- ----------------------------------- 
      2       Foo role       foo_role            Permission(s) 'delete any forum    
                                                 content, foo test permission' not  
                                                 found.                             
      2       Bar role       bar_role            Permission(s) 'bar test            
                                                 permission, edit any forum         
                                                 content' not found.                

    I would like to see each permission enclosed in single quotes. Just
    add single quotes to the glue parameter to
    import():

     +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php
     @@ -0,0 +1,95 @@
     ...
     +      // Log the message in the migrate map and the logger channel.
     +      $message = "Permission(s) '" . implode(', ', $invalid_permissions) . "' not found.";
     +      $this->migration->getIdMap()
  2. (same snippet) Despite the code comment, I do not see the
    messages in the Drupal log. Since there is not yet a way to see
    migration messages in the UI (using just Drupal core) I would like to do
    what the comment says.

I am removing the tags for IS and CR updates. I am leaving one item in the "Remaining tasks" section: review the CR.

quietone’s picture

212.1 Fixed.
212,2 In my opinion the comment is the mistake and I have changed it. I think we should keep the migration system as is where migrate messages are only logged when using the UI.

benjifisher’s picture

Status: Needs review » Reviewed & tested by the community

I reviewed and re-tested, and the patch in #213 adds the single quotes as intended. Thanks for fixing that!

I updated the change record, removing the instructions for reviewing the message at /admin/reports/dblog.

The only remaining task in the issue summary is to review the change record. I will not let that get in the way of marking this issue RTBC.

alexpott’s picture

Is there any reason we're not targeting 9.5.x too here? I can't think of why really. Also don't we need to take care of existing roles that have been migrated using the skip_missing_permission_deprecation flag?

quietone’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
3.99 KB
24.86 KB
30.04 KB

Added an update function (pretty much the same as the one added in #2571235: [regression] Roles should depend on objects that are building the granted permissions, which is in 9.5.x). And made a 9.5 patch.

alexpott’s picture

@quietone nice update function :)

The thing I don't understand is why this update wouldn't be part of 9.5.x too? I read the debate in the slack channel but I'm not sure why we'd wail till 10.0.x to run the update function.

benjifisher’s picture

Status: Needs review » Needs work

Comparing the two patches in #216, there are three things in the 10.0 patch that are not in the 9.5 patch:

  1. Add the update function, user_post_update_update_migrated_roles()
  2. Add a functional test for the update function.
  3. Remove the special handling of 'skip_missing_permission_deprecation' in Drupal\user\Entity\Role

The patch for 9.5 does remove 'skip_missing_permission_deprecation' from the two core role migrations.

I think these four changes should go together, so back to NW.

I am still bothered that in #2571235: [regression] Roles should depend on objects that are building the granted permissions we decided to issue deprecation notices in D9 when saving a user role with invalid permissions, but throw an exception notice in D10. That seems like an implicit promise that migrations would continue to work the old way until D10. But that conflicts with the promise that 10.0.0 will be "as close as possible" to 9.5.0, so I guess we have to choose the lesser evil.

quietone’s picture

Status: Needs work » Needs review
FileSize
2.07 KB
26.17 KB

@alexpott, thanks but I did not write the update function. It was added in #2571235: [regression] Roles should depend on objects that are building the granted permissions and I reused it in Drupal 10.

#218
1 and 2. The update function user_post_update_update_migrated_roles was not added to 9.5.x because user_post_update_update_roles exists in Drupal 9.5. The two functions are identical. The names are different because I got an error message (don't recall the details) in D10, so I changed the name. And, then there is no need for a new test because the test already exists.

3. Changed as requested.

catch’s picture

+++ b/core/modules/user/src/Entity/Role.php
@@ -202,8 +202,8 @@ public function calculateDependencies() {
     $invalid_permissions = array_diff($this->permissions, $valid_permissions);
-    if (!empty($invalid_permissions) && !$this->get('skip_missing_permission_deprecation')) {
-      @trigger_error('Adding non-existent permissions to a role is deprecated in drupal:9.3.0 and triggers a runtime exception before drupal:10.0.0. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '". Permissions should be defined in a permissions.yml file or a permission callback. See https://www.drupal.org/node/3193348', E_USER_DEPRECATED);
+    if (!empty($invalid_permissions)) {
+      throw new \RuntimeException('Adding non-existent permissions to a role is not allowed. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '".');
     }
     foreach ($valid_permissions as $permission) {

Not sure about making this change in 9.5.x. But if we don't throw the exception, then roles could start to accumulate invalid roles again, and we'd have to add another update in 10.x again to remove them again. Which suggests leaving it in 10.0.x to me.

Status: Needs review » Needs work

The last submitted patch, 219: 2953111-219.patch, failed testing. View results

benjifisher’s picture

From #219:

The update function user_post_update_update_migrated_roles was not added to 9.5.x because user_post_update_update_roles exists in Drupal 9.5.

I know this is an edge case, but suppose that a site has an ongoing migration that creates or updates user roles. And suppose that it takes advantage of the skip_missing_permission_deprecation option to add invalid permissions to those roles.

When the site was upgraded to 9.3, the invalid permissions were removed by user_post_update_update_roles(). But the migration continued to run periodically, and invalid permissions were re-introduced while the site used 9.3 and 9.4. Drupal keeps track of update functions and will not run them more than once, so those invalid permissions will persist when the site is upgraded to 9.5. We are removing the special processing of the option, so continued migrations will not add new, invalid permissions after upgrading to 9.5.

But the point of #220 is that the patch in #219 changes the behavior to match Drupal 10: throw an exception when saving one of the roles with invalid permissions. That can happen when submitting the Permissions form, leading to a WSOD.

As I said in #218, these changes should be made together. If we make the changes all at once, including a copy of the update function with a new name, then existing invalid permissions are removed when upgrading to 9.5, and then no new ones are created.

Maybe I was wrong about the test. If we have two identical functions with different names, then we do not have to test them both. I might make one a wrapper for the other, and have the test use the wrapper, but maybe I am being pedantic.

quietone’s picture

The 'new' update function is the same name as the one added in the other issue but with '_followup' at the end. It is used in both the D9 and D1o patches. I think that helps to understand why there are identical post_update functions. I have renamed the update test so it can be the same in D9 and D10 and added a few assertions.

I was going to add my thoughts on adding this to D 9.5 but I will refrain and let others decide. The twists and turns of this issue has taken it's toll.

benjifisher’s picture

I was going to add my thoughts on adding this to D 9.5 but I will refrain and let others decide.

I feel pretty strongly about my comment "these changes should be made together" in #218 and #223. My personal preference is to make them in 10.0 and not 9.5. But so far, I have not convinced @alexpott of that, so I guess we will make the changes in both branches.

I will review the patches in #223 after lunch.

For now, I am attaching an alternative interdiff for the 10.0.x patches in #216 and #223. The interdiff attached to #223 shows the renamed test as a deleted file and an added file. I generated mine by applying both patches to different branches, starting both from 10.0.x. Then I used git diff foo..bar.

benjifisher’s picture

Status: Needs review » Needs work

For the 10.0 version of the patch in #223, I have two minor complaints:

  1. I think we are testing removed permissions but not dependencies, so this summary line is not accurate.

     +++ b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleMigrateTest.php
     ...
     @@ -0,0 +1,65 @@
     +  /**
     +   * Tests that roles have dependencies and only existing permissions.
     +   */
     +  public function testRolePermissions() {
  2. I think the last three lines should be inside the if block. This is the same comment I made in #105.1.

     +++ b/core/modules/user/user.post_update.php
     @@ -14,3 +18,39 @@ function user_removed_post_updates() {
     +      $removed_permissions = array_diff($role->getPermissions(), $existing_permissions);
     +      if (!empty($removed_permissions)) {
              // ...
     +      }
     +      $permissions = array_intersect($role->getPermissions(), $existing_permissions);
     +      $role->set('permissions', $permissions);
     +      return TRUE;
benjifisher’s picture

I reviewed the patch in #223 for 9.5.x. The important thing is that the related changes are all made together, so I am satisfied there. Again, I have some minor requests:

  1. Same as #225.1.

  2. Same as #225.2. It is less clear in this branch, since it means updating an existing function (or copy/paste/update that function). It seems to me that updating an update function is fairly non-disruptive, but I will not insist if you disagree.

  3. Comparing the patches for 9.5 and 10.0, I notice that the 10.0 version uses the fixture for 9.4.0. Is there a reason to use the fixture for 9.3.0 here?

     +++ b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleMigrateTest.php
     @@ -0,0 +1,65 @@
     ...
     +  protected function setDatabaseDumpFiles() {
     +    $this->databaseDumpFiles = [
     +      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.3.0.bare.standard.php.gz',
     +    ];
     +  }
quietone’s picture

@benjifisher, thanks for reviewing again.

225. Everything here should be done.
226. Everything here should be done.

quietone’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 227: 2953111-227.patch, failed testing. View results

benjifisher’s picture

It looks as though there is a reason to use the 9.4.0 fixture in the 10.0 version. Both 9.3.0 and 9.4.0 fixtures are available in the 9.5.x branch.

quietone’s picture

Status: Needs work » Needs review
FileSize
647 bytes
30.4 KB

Restoring the 10.0.x patch
to use the 9.4.0 database dump.

Status: Needs review » Needs work

The last submitted patch, 231: 2953111-231.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review

Failed on an unrelated test, Drupal\Tests\book\Functional\BookTest::testGetTableOfContents, retesting

alexpott’s picture

+++ b/core/modules/user/src/Entity/Role.php
@@ -202,8 +202,8 @@ public function calculateDependencies() {
-    if (!empty($invalid_permissions) && !$this->get('skip_missing_permission_deprecation')) {
-      @trigger_error('Adding non-existent permissions to a role is deprecated in drupal:9.3.0 and triggers a runtime exception before drupal:10.0.0. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '". Permissions should be defined in a permissions.yml file or a permission callback. See https://www.drupal.org/node/3193348', E_USER_DEPRECATED);
+    if (!empty($invalid_permissions)) {
+      throw new \RuntimeException('Adding non-existent permissions to a role is not allowed. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '".');
     }

@catch pointed out to me that we shouldn't be changing this to an exception in 9.5.x - it'd be disagreeing with out past selves! I'm really really sorry I missed this.

I think the approach should be to get #231 in 10.0.x asap and then consider backporting the migrate changes without the update and Role changes to 9.5.x - so migrations to 9.5.x can take advantage of the new code. We might not have time to add this to 9.5.0 though... we'll see.

benjifisher’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
7.01 KB

I have reviewed the changes between #223 and #231, and they are pretty minor. They include the two changes I requested in #225 and renaming the post-update function.

Renaming the post-update function is something we still want to do, in case we do get this issue fixed for 9.5 as well.

@alexpott, I hope you will review #225.2. Maybe you already have. If I am right, then it fixes a mistake that we made in #2571235: [regression] Roles should depend on objects that are building the granted permissions.

I am marking this issue RTBC for the 10.0.x branch, although I have not done any manual testing since #212. I am attaching an interdiff comparing the patch from #207, which I tested, to the patch in #231.

mikelutz’s picture

+1 to #231 for 10.x

alexpott’s picture

Version: 10.0.x-dev » 9.5.x-dev
Status: Reviewed & tested by the community » Needs review

@benjifisher #225.2 is correct - nice spot!

Committed and pushed 247bd31efd to 10.1.x and 1002797d91 to 10.0.x. Thanks!

Setting back to 9.5.x to discuss if we want to backport only the migration and non update part of these changes. I'm not sure I could go either way and given the amount of time we have maybe it's best to just call this fixed in 10.0.0 and move on.

  • alexpott committed 247bd31 on 10.1.x
    Issue #2953111 by quietone, yogeshmpawar, ravi.shankar, benjifisher,...

  • alexpott committed 1002797 on 10.0.x
    Issue #2953111 by quietone, yogeshmpawar, ravi.shankar, benjifisher,...
benjifisher’s picture

I am happy to leave 9.5 the way it is. I think I said as much in #218.

In #217, @alexpott mentioned some discussions on Slack. Most or all of that discussion is on

in case anyone else wants to review it.

quietone’s picture

I too am happy to have this committed only to D10.

I was about to mark this Fixed but then I remembered the CR. It still needs to be reviewed.

benjifisher’s picture

Version: 9.5.x-dev » 10.0.x-dev
Issue summary: View changes
Status: Needs review » Fixed

I published the CR. I also closed (cannot reproduce) a related issue that has been Postponed (MNMI) for a while.

I think we have consensus, so I am changing the target version back to 10.0.x and marking this issue fixed.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.