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:
- After adding configuration, such as a content type, that has related permissions, provide a link to the module-specific Permissions form. See #2934995: Add a "Manage permissions" tab for each bundle that has associated permissions.
- Update the
help_topicsmodule and implementations ofhook_help(). Replace links to/admin/people/permissions#module-module_namewith/admin/people/permissions/module/module_name. Forhelp_topics, take advantage of #3192585: Fix up topics to use new help_topic_link function. - Move more duplicated code from the Modules form and the Confirmation form to the new trait.
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:

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

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).
| Comment | File | Size | Author |
|---|---|---|---|
| #43 | 3216341-43.patch | 18.58 KB | benjifisher |
| #20 | Before patch message.png | 40.37 KB | manojithape |
| #20 | After patch module specific permission form.png | 127.12 KB | manojithape |
| #17 | new-permission-form.png | 77.85 KB | benjifisher |
| #17 | modules-enabled.png | 17.74 KB | benjifisher |
Issue fork drupal-3216341
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
Comment #3
benjifisherThe 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
$formarray fromUserPermissionsForm::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.
Comment #4
benjifisherI added these two lines to the route requirements:
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?
Comment #5
berdir> 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.
Comment #6
berdirAlso, 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.
Comment #7
aaronmchaleThanks 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
Comment #8
benjifisher@Berdir, @AaronMcHale, thanks for the early feedback!
If the module is not installed, then
$this->permissionHandler->moduleProvidesPermissions($module)returnsFALSE, so access denied (in the current MR). What if a module is installed but does not provide any permissions? Currently the access function returnsAccessResult::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
fpamodule is pretty complicated, modifying the existing Permissions form. Thepfmmodule 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 thepfmlets 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.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, andfield_uimodules 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/modulesto 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.
Comment #9
benjifisherIn the System module, both
ModulesListFormandModulesListConfirmFormgenerate 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->currentUserand$this->formatPlural()in the trait? Or should I declare properties$currentUserand$stringTranslationin the trait? Or should I pass those to the helper function?The main form, but not the confirmation form, already injects the
access_managerservice. 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 thehelp_topicsmodule 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.Comment #10
benjifisherI 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.
Comment #11
berdirI'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.
Comment #12
benjifisher@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.
Comment #13
benjifisherComment #14
benjifisherI 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/permissionsfor this task. I want to make it easy to follow the standard advice. My approach to this issue is influenced by this use case:Comment #15
benjifisher@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.
Comment #16
benjifisherI am adding details to the "Proposed resolution" section of the issue description.
Comment #17
benjifisherI am adding screenshots to the issue description.
Comment #18
benjifisherFollow-up to part of #9:
I added
use StringTranslationTraitto make sure that$this->formatPlural()is available. It should not be a problem thatFormBasealready does that.I added
abstract protected function currentUser();. That function is defined inFormBase, but my new trait does not "know" that. Since it usescurretnUser(), it should declare it.Comment #19
manojithape commentedComment #20
manojithape commentedVerified 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:
Expected Results:
Actual Result:
Please refer to the attached screensnshots.
We can move this ticket to RTBC.
Comment #21
manojithape commentedComment #22
benjifisher@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
UserPermissionsTestto 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.
Comment #23
benjifisherI 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.
Comment #24
longwaveA few bits of feedback on the MR but this is looking like a nice feature.
Comment #25
benjifisher@longwave, thanks for the review!
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.
Comment #26
longwaveThanks! 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!
Comment #27
longwaveOne 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.
Comment #28
benjifisherI 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 nowforeach ($this->permissionsByProvider() as $provider => $permissions). Unless we want to rewritebuildForm(), 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 togroupPermissions().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.)
Comment #29
longwaveIt looks to me like we could simplify the special casing of node with something like:
and
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.Comment #30
benjifisherI 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():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 inbuildForm(). This issue moves that code out ofbuildForm(), and a future issue can provide an alter hook. Either way, I would like to keep that complexity out of the form method.Comment #31
benjifisherI 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.
Comment #32
longwaveIn 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.
Comment #33
benjifisherI 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.
Comment #34
benjifisherI should use
git diff 9.3.x...when generating a patch, notgit diff 9.3.x... I should also look at the patch before uploading it.Comment #35
benjifisherFrom #26:
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.
Comment #36
benjifisherOne 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.
Comment #37
aaronmchaleRe #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
Comment #38
benjifisherI 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.
Comment #39
larowlanLooks 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
Comment #40
benjifisher@larowlan:
Thanks for the review!
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.
Comment #41
benjifisherI 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.
Comment #42
longwaveAll 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.
Comment #43
benjifisherHere is a copy of the current diff as a patch.
Comment #44
larowlanThis looks so close. Just a super minor adjustment to add return types for new code. Feel free to self-RTBC for those changes
can we get return types added here (should be added for all new code)
Here too
Comment #45
benjifisher@larowlan:
I missed that change, but I like it. From Coding standards > Parameter and return typehinting:
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
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? ;)
Comment #46
larowlanTechnically yes, but it doesn't really give us much benefit to add
:voidto all of them, something we can do in bulk when we get to phpstan level 6 some time in the distant futureComment #47
larowlanIssue credits
Comment #48
larowlanThis 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.
Comment #49
benjifisherI added follow-up issues:
The first three were mentioned as remaining tasks in the issue summary. (I am removing those tasks.) The last was suggested in #36, #37.
Comment #50
benjifisherOops, I made a mistake in the
@returncomment:Comment #52
larowlan