Problem/Motivation

The Permissions form (/admin/people/permissions) is a usability nightmare. With a checkbox for each combination of permission and role, it can be a performance problem as well.

Let's not try to solve the problem all at once, but try to provide an alternative to using the catch-all Permissions form. We already have a version of the form for a single role, such as /admin/people/permissions/anonymous. Let's also provide a version of the form that applies to just a selected list of modules.

The module-specific Permissions form will be useful in at least these situations:

  • Target of links from the Extend page (/admin/modules). Currently those links go to the appropriate anchor on the full Permissions form.
  • After installing one or more modules, provide a link to a page with just the newly available permissions.
  • After adding configuration, such as a content type, that has related permissions, provide a link to the module-specific Permissions form. (follow-up issue)

Proposed resolution

Provide a form at /admin/people/permissions/module/%, where the parameter can be a single module name or a comma-separated list of module names.

Add a custom access checker for the new form. Deny access unless at least one of the requested modules defines at least one permission. This way, code that provides a link to the new form can check access before adding the link.

Provide a link to the new form after enabling one or more modules. This requires changes to both ModulesListForm and ModulesListConfirmForm, so add a trait for code that is shared by those two classes.

Remaining tasks

Add follow-up issues:

User interface changes

Existing links on /admin/modules will go to instances of the new form.

The message after installing a module (or more than one) will include a link to the new form. Here is a screenshot of the message after enabling the Content Moderation and Workflows module:

 Content Moderation, Workflows. Configure related permissions."

Here is a screenshot of the form showing permissions for those two modules:

 Content Moderation and Workflows

API changes

None

Data model changes

None

Release notes snippet

Site administrators can view and edit permissions for a single module or a list of modules. The Permissions links on the Extend page (/admin/modules) now go to these pages. When a site administrator enables one or more module from the Extend page, and at least one module provides permissions, the confirmation message includes a link to the page for the newly enabled module(s).

Issue fork drupal-3216341

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

benjifisher created an issue. See original summary.

benjifisher’s picture

The role-specific form uses the path /admin/people/permissions/{user_role}. The proposed path /admin/people/permissions/module/{modules} does not conflict with this.

It is not hard to create the new form. The MR for this issue already does that.

Implementation note: the $form array from UserPermissionsForm::buildForm() uses both module names and permission names as top-level keys. This means there is no easy way to generate that form and then restrict to the specified list of modules. We have to add a few lines of code to the parent form class.

When specifying more than one module, the form sections will be in the same order in the new form as in the original form. The order that modules are listed in the path makes no difference.

Because of the proposed path, the main Permissions form is available as a breadcrumb. If you have a role named Module (more precisely, if the machine name is "module"), then there will also be a breadcrumb (with link text "Edit role") to the Permissions form for that role.

benjifisher’s picture

I added these two lines to the route requirements:

    modules: '[a-z][a-z_,]*'
    _custom_access: '\Drupal\user\Form\UserPermissionsModuleSpecificForm::access'

The first line is a sloppy way to say "one or more valid module names, separated by commas". I could make a more complicated regular expression that comes closer, but I do not think it is worth it. More important: what is a valid module name? The best reference I could find was Naming and placing your Drupal module, which tells me that the name can up to 50 lower-case letters and underscores, starting with a letter. There are some other restrictions.

The second line defines a custom access function, which checks whether there are any permissions defined by any of the modules on the list. I am not sure that access is the right mechanism to use, but the nice thing is that it will avoid producing links to pages that have no permissions on them. Basically, this access function takes care of the checks that are currently made in Drupal\system\Form\ModulesListForm::buildRow().

Do I have to worry about caching the access result, or will that cache be cleared whenever a module is (un)installed?

berdir’s picture

> More important: what is a valid module name?

I don't think we need to bother with a regular expression there at all. We're not interested in all technically allowed module names, the only valid arguments are existing, installed module. I'd just check if the module is installed, if not, access denied in your custom access check.

berdir’s picture

Also, might be worth looking at some existing contrib modules for this, like https://www.drupal.org/project/fpa. There are also others that work a bit differently, like https://www.drupal.org/project/pfm.

aaronmchale’s picture

Thanks for working on this Benji, this is going to be such a valuable addition!

So, I like that the user can essentially construct a path like /admin/people/permissions/module/foo,node,bar,contact, gives some nice flexibility here. What I'm now wondering is, does that mean it would be the responsibility of the Permissions links on the module page to appropriately construct the URL with all relevant dependencies?

For example, the Node module depends on: Text, Field, Filter, User, System. So should the URL it constructs look like this: /admin/people/permissions/module/node,text,field,filter,user,system?

On one level, that would make sense, because I could reasonably see a situation where someone enables a module but is required to configure permissions from a different module (Field UI is an example of that); But practically speaking, when you enable Node, there's no permissions in User that are practically relevant here, and actually that combination of modules results in a lot of permissions which are not relevant to Node.

Sticking with the field_ui example, if I enable Node, Node doesn't actually depend on field_ui, but there are permissions that the field_ui module generates, under its own heading on the permissions screen, which are directly related to Node, for example: "Content: Administer display", "Content: Administer fields", and "Content: Administer form display". The user should, at the very least, know about these permissions, but because Node doesn't depend on field_ui we don't have a programmatic way to generate a URL which will include those permissions.

Now, to be honest, my specific example here, might not be an issue in the future, when #3193983: Move entity permissions to the module that is providing the entity lands. But, that issue only addresses Core modules. It's not unreasonable to assume that because of how flexible our permission system is, some Contrib modules may be doing something similar. Having said that, do we really need to worry about that, should that just be up to Contrib modules to sort out and make sure they've got their dependencies and permissions all in order? Probably, but it's at least worth keeping in mind.

Alright, this kind of turned into a brain dump, but hopefully it's a useful one :)

Thanks,
-Aaron

benjifisher’s picture

Status: Active » Needs work

@Berdir, @AaronMcHale, thanks for the early feedback!

I'd just check if the module is installed, if not, access denied in your custom access check.

If the module is not installed, then $this->permissionHandler->moduleProvidesPermissions($module) returns FALSE, so access denied (in the current MR). What if a module is installed but does not provide any permissions? Currently the access function returns AccessResult::forbidden(). I think this is a good thing. If I change that, then we will generate links to pages like /admin/people/permissions/module/ckeditor, which does not seem useful. Besides, doing it this way saves me the trouble of deciding what to do in the case: "Sorry, the ... module does not provide any permissions."

I had a look at the two modules you suggested. The fpa module is pretty complicated, modifying the existing Permissions form. The pfm module is similar to the approach I am taking. It creates a single new page instead of one per module. I think a feature of my approach is that each page has an access checker, as above. It is nice that the pfm lets you filter on module or role or both. It is not nice that it duplicates most of the logic of the core Permissions form. Since I am doing it as a core patch instead of a contrib module, I can take a simpler approach, adding a few lines to the existing form class.

... does that mean it would be the responsibility of the Permissions links on the module page to appropriately construct the URL with all relevant dependencies?

I will read that question as applying to any situation where we create a link to the module-specific permissions page.

After reading the rest of #7, I think the answer is yes. (My bias is to give the same answer, since that keeps this patch simpler.) Considering the node, user, and field_ui modules shows that automatically including modules required by the requested modules is not the right answer.

My next step will be to update the links on /admin/modules to go to the new pages. Each module will have a link to the page for just that module. I suggest that doing anything more complicated is out of scope for this issue.

I also see problems with including other modules. For example, the CKEditor module does not provide any permissions, so there will be no link (same as current behavior). But it depends on the Text Editor, Filter, User, System, File, and Field modules. I think it is confusing for CKEditor to provide a link to a permissions page that has permissions for all of those modules but does not even mention CKEditor.

benjifisher’s picture

In the System module, both ModulesListForm and ModulesListConfirmForm generate a confirmation message when enabling modules. Instead of adding to the duplicated code, I am adding a trait and generating the confirmation message there.

Is it OK to just use $this->currentUser and $this->formatPlural() in the trait? Or should I declare properties $currentUser and $stringTranslation in the trait? Or should I pass those to the helper function?

The main form, but not the confirmation form, already injects the access_manager service. I assume this is more performant than $url = Url::fromRoute(...) and then checking $url->access(...). Is it worth injecting the service into the confirmation form so that I can use it in the new trait?

There are a lot of links in implementations of hook_help() and in the help_topics module that go to the main Permissions page with a specific fragment (anchor). I am willing to update all of those as part of this issue, or do so in a follow-up issue.

benjifisher’s picture

Status: Needs work » Needs review

I think this issue is ready for review. I have done everything I suggested in the issue description, except that I only covered the most common use case (new content type) of configuration that adds new permissions.

@Berdir, if you feel strongly about either of the suggestions you made in #5, and you are not convinced by #8, then I will take your advice.

berdir’s picture

I'm perfectly fine with the has-permissions check. I didn't look at the code yet, I'm arguing to keep things as simple as possible.

Didn't even see that you support multiple modules, but strong +1 to not do anything special with that out of the box, at least not in the initial issue. Too many attempts at improving permissions, both the UI and technical things got stuck on trying to do too much. I don't think dependencies are a good indicator to decide what modules to list there, that's going to show both too many and not enough permissions. Follow-ups or contrib can experiment with that.

benjifisher’s picture

Issue summary: View changes
Issue tags: +Needs followup

@Berdir, thanks for the review! I implemented two of your requested changes and replied to the other two.

I am adding the issue tag for follow-up issues, and I will add details in the issue summary.

benjifisher’s picture

Assigned: benjifisher » Unassigned
benjifisher’s picture

Issue summary: View changes

I added some test coverage.

There is duplicated code in the Permissions form and the confirmation form. This issue adds a trait to hold some of that code. I am adding a note to the issue summary to move more of the duplicated code, in a follow-up issue.

To be clear, the link after installing a module (or more than one) is an important part of this issue. The standard advice after enabling a module is to configure permissions for it. Currently, we direct site administrators to the overwhelming /admin/people/permissions for this task. I want to make it easy to follow the standard advice. My approach to this issue is influenced by this use case:

  • Create a page with permissions for one or more selected modules.
  • Make it simple for the calling function. For example, the calling function does not have to check which of the modules provide permissions.
benjifisher’s picture

@dww, thanks for the review!

I notice when the issue is updated. I guess I will have to watch the MR, too. If this issue had been marked NW, then it would be back to NR now.

benjifisher’s picture

Issue summary: View changes

I am adding details to the "Proposed resolution" section of the issue description.

benjifisher’s picture

Issue summary: View changes
StatusFileSize
new17.74 KB
new77.85 KB

I am adding screenshots to the issue description.

benjifisher’s picture

Follow-up to part of #9:

I added use StringTranslationTrait to make sure that $this->formatPlural() is available. It should not be a problem that FormBase already does that.

I added abstract protected function currentUser();. That function is defined in FormBase, but my new trait does not "know" that. Since it uses curretnUser(), it should declare it.

manojithape’s picture

Assigned: Unassigned » manojithape
manojithape’s picture

Verified and tested patch MR !732 mergeable (i.e. https://git.drupalcode.org/project/drupal/-/merge_requests/732.patch) on the drupal 9.3.x-dev version. Patch applied successfully and looks good to me.

Testing Steps:

  1. Install drupal 9.3.x-dev version.
  2. Install Workflow and Content Moderation module and observe the module permission link not displayed in the confirmation message.
  3. Now apply the patch and clear the cache.
  4. Again install the same modules and verify the Configure module "related permission" link displayed in confirmation message and by clicking on user redirect to "/admin/people/permissions/module/% " module permission page.
  5. Verify that the user can update module-specific permissions by using the module-specific permissions form.

Expected Results:

  1. "Related permission" link should display in the confirmation message of module installation and by clicking on that user should redirect to the "/admin/people/permissions/module/% " module permission page.
  2. User should able to update module-specific permissions by using the module-specific permissions form.

Actual Result:

  1. "Related permission" link displayed in the confirmation message of module installation and by clicking on that user redirect to the "/admin/people/permissions/module/% " module permission page.
  2. User is able to update module-specific permissions by using the module-specific permissions form.

Please refer to the attached screensnshots.
We can move this ticket to RTBC.

manojithape’s picture

Assigned: manojithape » Unassigned
benjifisher’s picture

Issue tags: +Needs change record

@mannojithape:

Thanks for testing, and for the detailed description of your testing steps. I am glad to see that you tested submitting the form. That is very important! I added some steps to UserPermissionsTest to check the same thing.

Based on #20, I would say that this issue is tested, but not yet reviewed. It needs both before we can call it RTBC.

I think any issue that makes a change to the UI is supposed to have a change record, so I am adding an issue tag for that.

benjifisher’s picture

StatusFileSize
new29.93 KB

I am uploading the current diff from the MR as a patch. This way, people can apply the patch for testing purposes without being affected by possible further changes to the MR.

longwave’s picture

A few bits of feedback on the MR but this is looking like a nice feature.

benjifisher’s picture

@longwave, thanks for the review!

  1. I added the test you suggested.
  2. I explained why I prefer to break the long line.
  3. I explained why it is simpler to filter the list of modules before building the form and suggested an alternative.

I implemented the alternative suggested in (3) locally, but have not pushed it to the remote. It passes the two tests touched by this issue. If you like the idea, then I can push my local changes.

longwave’s picture

Status: Needs review » Needs work

Thanks! I think the test changes are OK, there is no need to add a whole bunch of code when we can piggyback on something that already exists. Agree that while data providers are good we can't really use them in functional tests.

I like the permissionsByProvider() approach, this both makes buildForm() less complex and also allows subclasses to modify the functionality as we do here. Flagging as needs work to try this.

I also wonder if #763074: Add hook_permission_alter() so that permission descriptions and other properties can be altered would allow node to take ownership of the access content permission, and that would entirely sidestep the special case here, so we wouldnt need the event or anything else. Changing the provider of a permission may have other side effects though!

longwave’s picture

One way of keeping the node special case in the parent might be to rename the method groupPermissions() and pass in the permission list as an argument, with the node case already overridden by that point? That also leaves us open to a subclass ordering the permissions in a completely different way we haven't thought of.

benjifisher’s picture

Status: Needs work » Needs review

I just updated the MR with the permissionsByProvider() method so you can see more specifically what it does. (I made some changes to the commit since my previous comment, so I am glad I waited.)

I do not think it is a good idea to alter the result of getPermissions() in order to change permissions on this page. As you say, that might have other side effects. I prefer a more specific alter hook. Also, there does not seem to be much momentum behind #763074: Add hook_permission_alter() so that permission descriptions and other properties can be altered.


In the snippet that appears above, foreach ($permissions_by_provider as $provider => $permissions) is now foreach ($this->permissionsByProvider() as $provider => $permissions). Unless we want to rewrite buildForm(), we really need the permissions grouped by module (provider). Maybe I do not understand you suggestion, but so far I do not like the idea of changing the name to groupPermissions().

Can you explain a little more your idea for passing in the permission list as an argument? I do not see how that would help. If the goal is to move that code to the Node module, then we have to provide an alter hook somewhere. Where would you put it? (To be clear: that change would not be part of this issue.)

longwave’s picture

It looks to me like we could simplify the special casing of node with something like:

    $all_permissions = $this->permissionHandler->getPermissions();
    if ($this->moduleHandler->moduleExists('node')) {
      $all_permissions['access content']['provider'] = 'node';
    }
    foreach ($this->permissionsByProvider($all_permissions) as $provider => $permissions) {

and

  protected function permissionsByProvider($permissions) {
    $permissions_by_provider = [];
    foreach ($permissions as $permission_name => $permission) {
      $permissions_by_provider[$permission['provider']][$permission_name] = $permission;
    }
    return $permissions_by_provider;
  }

Though I guess simplifying this code might be considered out of scope for this issue - and maybe it doesn't really matter where we do the special casing if we want to get rid of it somehow anyway.

I guess I was wrong about renaming it groupPermissions() as the rest of buildForm() always groups by provider anyway, you would need a bigger override to change this.

benjifisher’s picture

I do consider that simplification to be out of scope for this issue. But I am willing to make changes to support simplification in a future issue. For example, we should decide now whether to pass in the permissions list as an argument.

We can also make the same simplification inside permissionsByProvider():

  protected function permissionsByProvider() {
    $permissions = $this->permissionHandler->getPermissions();
    $permissions_by_provider = [];

    // Move the access content permission to the Node module if it is installed.
    // @todo Add an alter so that this section can be moved to the Node module.
    if ($this->moduleHandler->moduleExists('node')) {
      $permissions['access content']['provider'] = 'node';
    }

    foreach ($permissions as $permission_name => $permission) {
      $permissions_by_provider[$permission['provider']][$permission_name] = $permission;
    }

    return $permissions_by_provider;
  }

That moves the permission from System to Node, but puts it in alphabetical order instead of before 'view own unpublished content'. I do not think we want to change that, certainly not as part of this issue.

Whether or not we simplify the Node-specific code, I think it should be done inside the permissionsByProvider() method. If we pass the permissions list as an argument, then the alteration has to be done in buildForm(). This issue moves that code out of buildForm(), and a future issue can provide an alter hook. Either way, I would like to keep that complexity out of the form method.

benjifisher’s picture

Issue summary: View changes
Issue tags: -Needs change record

I added the draft change record Permissions can be viewed and edited for one module or a list of modules. I also added a Release notes snippet to the issue summary.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

In that case there is no point passing in the permissions as an argument, I think.

Thanks for the thoughtful discussion about this feature. I think all points in the MR have been resolved and so this is ready for core committer review.

benjifisher’s picture

StatusFileSize
new1.79 MB

I am happy to have that discussion. I think the last few commits improved the implementation.

I am uploading the changes from the current version of the MR as a patch in order to make it easier to test.

benjifisher’s picture

StatusFileSize
new18.13 KB

I should use git diff 9.3.x... when generating a patch, not git diff 9.3.x... I should also look at the patch before uploading it.

benjifisher’s picture

From #26:

Changing the provider of a permission may have other side effects though!

Maybe I am beating a dead horse here (*) but now that #2571235: [regression] Roles should depend on objects that are building the granted permissions has been fixed, I can think of an important side effect. Saving any role with the "access content" permission will recalculate dependencies, making the role depend on the Node module. If the Node module is then uninstalled ... what happens? I think the permission is removed, and the role is saved. That would be bad: all of a sudden, some roles lose the "access content" permission.


(*) "Beating a dead horse" is an idiom for continuing to fight after you have already won.

benjifisher’s picture

One suggestion that was made on Slack is to change the title of the new pages. Currently, the tile is "Module Permissions", the same as the main permissions page (/admin/people/permissions).

The question is what to use for the title. If there is just one module, then something like "Node Permissions" would work. If there are a few modules, maybe it should be "Permissions for Content Moderation, Node, and Workflows Modules". If there are many modules, then this option leads to a long title.

An alternative is to leave the title as is, but provide a description at the top of the form, listing the modules on the page. But does this add information to the page or is it just noise?

If we can agree on a good suggestion, then I am happy to implement it as part of this issue. We could also add a follow-up issue to continue the discussion.

aaronmchale’s picture

Re #36: I think that was me that suggested that (unless it was also suggested by someone else), in my opinion I'd be happy to have that pushed to a follow-up so it can be properly discussed and prototyped without delaying this issue.

Everything about this issue is an excellent improvement and we should get this in as soon as is reasonable.

Thanks,
-Aaron

benjifisher’s picture

Issue summary: View changes

I see that #3192585: Fix up topics to use new help_topic_link function is fixed (and included in Drupal 9.2.3). That will affect one of the proposed follow-up issues, so I am adding a link in the issue summary.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Looks great, this is a killer feature.
Left some comments on the MR.

There's a number of unresolved MR threads too, would be good to mark the resolved ones as such

benjifisher’s picture

@larowlan:

Thanks for the review!

There's a number of unresolved MR threads too, would be good to mark the resolved ones as such

I prefer to let the one who opened the thread (the reviewer) mark the thread as resolved, but I will resolve them myself if that is OK.

I will try to update the MR after work today. But I am going on vacation tomorrow, so I might not get to it for another week.

benjifisher’s picture

Status: Needs work » Needs review

I resolved all of the old threads on the MR, since @larowlan already had a chance to review them. I made changes for the new threads and left them open.

Back to NR.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

All points addressed, back to RTBC. I think the regular expression in the route requirements could go either way, we don't really need it and we don't tend to be as restrictive elsewhere, but it also doesn't seem to hurt anything here.

benjifisher’s picture

StatusFileSize
new18.58 KB

Here is a copy of the current diff as a patch.

larowlan’s picture

This looks so close. Just a super minor adjustment to add return types for new code. Feel free to self-RTBC for those changes

  1. +++ b/core/modules/user/src/Form/UserPermissionsForm.php
    @@ -81,6 +81,39 @@ protected function getRoles() {
    +  protected function permissionsByProvider() {
    
    +++ b/core/modules/user/src/Form/UserPermissionsModuleSpecificForm.php
    @@ -0,0 +1,67 @@
    +  protected function permissionsByProvider() {
    

    can we get return types added here (should be added for all new code)

  2. +++ b/core/modules/user/src/Form/UserPermissionsModuleSpecificForm.php
    @@ -0,0 +1,67 @@
    +  public function buildForm(array $form, FormStateInterface $form_state, $modules = '') {
    ...
    +  public function access($modules) {
    

    Here too

benjifisher’s picture

@larowlan:

I missed that change, but I like it. From Coding standards > Parameter and return typehinting:

Parameter and return typehints should be included for all new functions and methods, including new child implementations of methods for existing classes and interfaces.

Does that apply to test methods? I added a new test in two different classes, but you did not mention them.

I updated the MR, adding return types to the four functions you mentioned in #44 and also

+++ b/core/modules/system/src/Form/ModulesEnabledTrait.php
@@ -33,7 +34,7 @@ abstract protected function currentUser();
...
-  protected function modulesEnabledConfirmationMessage(array $modules) {
+  protected function modulesEnabledConfirmationMessage(array $modules): PluralTranslatableMarkup {

By the way, the Drupal docs refer to "typehints", but https://www.php.net/manual/en/language.types.declarations.php in the PHP docs calls them "Type declarations". I think there used to be a note on that page about previously calling them "hints", but I searched the page just now, and "hint" appears only in a user comment.

Since you left the issue status at RTBC, I cannot use the dispensation you granted me. Can I save that for next time? ;)

larowlan’s picture

Does that apply to test methods? I added a new test in two different classes, but you did not mention them.

Technically yes, but it doesn't really give us much benefit to add :void to all of them, something we can do in bulk when we get to phpstan level 6 some time in the distant future

larowlan’s picture

Issue credits

larowlan’s picture

This looks good to go to me, we just need one or two more follow-ups filed per the remaining tasks - some of them don't appear to have issue links yet.

benjifisher’s picture

benjifisher’s picture

Oops, I made a mistake in the @return comment:

+++ b/core/modules/user/src/Form/UserPermissionsForm.php
@@ -84,7 +84,7 @@ protected function getRoles() {
   /**
    * Group permissions by the modules that provide them.
    *
-   * @return string[][][]
+   * @return string[][]
    *   A nested array. The outer keys are modules that provide permissions. The
    *   inner arrays are permission names keyed by their machine names.

  • larowlan committed 217f555 on 9.3.x authored by benjifisher
    Issue #3216341 by benjifisher, manojithape, longwave, larowlan, Berdir,...
larowlan’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs followup

Status: Fixed » Closed (fixed)

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