Problem/Motivation
Currently, all permission checks go through:
- User::hasPermission()
- UserSession::hasPermission()
Both check for UID1 and then call the role storage's isPermissionInRoles() method. This means that all of the code is contained in a class we can't alter and a handler we can't easily alter (only one module can swap it out). The upside is that all of core's permission checks eventually make it to either of those functions.
So why don't we move these checks to a service? If we make sure all permission checks happen in for example a permission_checker service, then we can more easily intervene in permission checks by decorating said service and doing our thing.
I'm asking this because we currently have 2 types of access checks:
- Permission checks, which is Role Based Access Control (RBAC)
- Entity access, where we use Attribute Based Access Control (ABAC) mixed with RBAC
The latter we can easily interact with using hook_entity_access, the former we can't. Modules such as Group, Domain, etc. could be significantly more powerful if we could also interfere with the former. We could have a proper Policy Based Access Control (PBAC) system like this, e.g.: Editors can only edit when on certain machines or during office hours.
Furthermore, if we combine Flexible Permissions with this patch, the Domain module could become insanely simple in architecture. Words cannot describe how awesome this could be.
Proposed resolution
- Move the code from User(Session)::hasPermission() to a new service and that's it.
- Potentially mark the old methods as deprecated in a future Drupal version and force people to properly inject said service.
Remaining tasks
Review, discuss and potentially adjust the PoC
User interface changes
None
API changes
New service, no BC break
Data model changes
None
Release notes snippet
TBD
| Comment | File | Size | Author |
|---|---|---|---|
| #35 | 3347873-nr-bot.txt | 90 bytes | needs-review-queue-bot |
Issue fork drupal-3347873
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 #2
kristiaanvandeneyndeComment #4
kristiaanvandeneyndeProof of concept added
Comment #5
andypostComment #6
andypostComment #7
smustgrave commentedThis seems like a great idea!
Tagging for framework manager for their thoughts on it.
Wasn't sure if this will need special tests or if the current tests work.
Comment #8
borisson_I don't think this needs specific test coverage, so much of our tests already having permissions needed to function.
I was going to rtbc this issue, because it's a simple refactor. The code was only there 2 times - instead of the 3 usually needed for something to be refactored away, but I think the argument for this code needing to decorated by multiple modules is strong in this case.
I can't actually move this to rtbc, but consider this a +1.
Also started a CR.
Comment #9
kristiaanvandeneyndeRegarding tests: I had to ever so slightly change one test that was testing the code that is now moved to the service. As @borisson_ said: Any test that calls for a permission check inherently also covers this code.
Removed the tags that are therefore "done" if that's alright.
Comment #10
kristiaanvandeneyndeAnother great example of what this could allow (with Flexible Permissions or similar): Only get "dangerous" permissions once your account has 2FA enabled.
You could also turn off UID 1's god mode switch, revoke permissions based on certain policies, impose content limits (deny "add comment" after spam is detected, etc.
Comment #11
smustgrave commentedUnderstood on the tests.
Still think it will need a change record as a new service is being added.
Comment #12
kristiaanvandeneyndeDidn't @borisson_ already add that? Or does the tag need to stay either way?
Comment #13
borisson_There is a cr here: #3348054: Permissions checking has been centralized to a new PermissionChecker service, I understand that this is fairly short, but without going super deep into groups/domains/flexible permissions/simple oauth like things, this doesn't actually do a lot, so to keep the CR based on what core does; it is just a way to extract the current duplicated code to an overridable service.
Comment #14
smustgrave commentedThink it would be useful to have a before/after code snippet in the CR
Before
You had to do xyz
Now
You just call this neat new service.
But with code examples.
Sorry for missing the CR before.
Comment #15
kingdutchThis would be a great addition to Drupal core :) At Open Social we've previously had discussions where we'd love to be able to augment all
hasPermissioncalls and this issue would make that possible.We eventually implemented a workaround in the
simple_oauthmodule which decorates the User, but that's quite a heavy handed approach just to change permission checking. Being able to swap out a service would've been a great help.The patch itself looks good and I agree that Drupal's current tests should already cover this (since the whole point of the issue is making a drop-in replacement that provides more extensibility).
The only thing that's stopping me from moving this to RTBC is the
PermissionCheckername of the class. Though I think it's a great name for the interface, I think for the class it's too generic and suggests only a single right way to do it. My proposal would be to rename it toRoleBasedPermissionCheckerwhich better communicates its current functionality and that other implementations are possible. The uid 1 check makes it do slightly more than just role based permission checks, but I think since we want to get rid of the UID 1 behaviour that's ignorable (#540008: Add a container parameter that can remove the special behavior of UID#1), or handled with a comment "Drupal assumes user 1 has all roles/permissions".If the above is a problem and we think
RoleBasedPermissionCheckerdoesn't sufficiently cover the class' behavior then we could also go withDefaultPermissionCheckerwhich would also communicate it's a Drupal default and could be swapped out.Comment #16
kingdutchI don't think there's a sensible code snippet to add to the CR since the whole idea of the issue is that all the calling code remains exactly the same. I've slightly altered the wording of the CR to put more emphasis on the advanced use-cases (which Domain and Group are) :)
Comment #17
kristiaanvandeneyndeRe #15:
I'm more fan of calling it DefaultPermissionChecker right now as RoleBased does not cover what's currently happening.
In a later update we could change it to RoleBasedPermissionChecker and decorate it with User1AccessPermissionChecker. This service could then be opted out of at first (e.g. D11) to allow people to turn off this dreaded god-mode switch and in an even later update (e.g. D12) have the god mode decorator stay, but be turned off by default. Then people who locked themselves out of their site only need to temporarily enable said service to regain access.
Comment #18
kristiaanvandeneyndeUpdated the CR with example code as well as I could :D
Comment #19
catchThis seems reasonable to enable contrib overrides.
I think PermissionChecker is fine for the naming here - we don't usually prefix single core services anticipating how contrib might swap them out.
I tried to think of a way to not have to call \Drupal::service() - like adding setter injection and then injecting the role manager before calling the method, but of course that would completely undermine the point of this being swappable, so... best we can do.
I think the permission checker interface needs extra documentation - i.e. it should have some kind of warning about swapping it out that you become responsible for security etc. Needs work for those extra docs, but untagging for framework manager review.
Comment #20
kristiaanvandeneyndeAdded the requested information to the interface.
Comment #21
kingdutchI'm content to skip the renaming of the class as per #19. The update in the CR and the interface provide good additional context.
With those changes I'm happy to see this go in :D
Comment #22
bramdriesenOne question though.
Doesn't the following:
$this->entityTypeManager->getStorage('user_role')->isPermissionInRoles($permission, $account->getRoles());Always return TRUE for user 1? Making the IF uid = 1 check redundant? Or is that there for performance reasons? I have to admit I didn't test or look into the
isPermissionInRoles()function 😉.Just asking because I'm usually against the UID=1 check in my own projects.
Comment #23
kristiaanvandeneyndeRe #22, no it does not:
I'm also against the UID 1 check by the way, but this issue is not the place to get rid of that. #540008: Add a container parameter that can remove the special behavior of UID#1 is.
Comment #24
bramdriesenWhich is used by
isPermissionInRolesdoes that no?The issue you linked to remove that logic bottomline. But here we are adding it to a service which I think does not need this check.
Comment #25
kristiaanvandeneyndeNo, that isAdmin() check is to see if the role is flagged as admin. Anyone could have that role, it's not limited to UID1.
Comment #26
kristiaanvandeneyndeComment #27
bramdriesenI still think the if UID=1 could be removed.
I just tested and
\Drupal::currentUser()->hasPermission('some permission');always returns TRUE for user 1. Making the check thus redundant. Even if user 1 doesn't have the administrator role assigned to him that function returns TRUE.Comment #28
bramdriesenComment #29
kristiaanvandeneyndeWith all due respect, Bram, but you are wrong here.
The code you are quoting is the exact same code that is being moved to this new service. Of course it works like that without this patch, it is the very same piece of code! You can even see this because I initially removed the
(int)type-casting of UID 1 in this MR and over a hundred tests failed. When I reintroduced the casting, it immediately went fully green.I honestly don't see how we could have a UID 1 check in core now, move the code VERBATIM to another place and then decide to change it so user 1 loses their god mode. Please try the patch locally and then remove the UID 1 check. You'll see your core tests will fail.
Comment #30
bramdriesen🤦♂️ Think I was sleeping haha. You are indeed correct.
In that case RTBC+++!
Comment #31
kristiaanvandeneyndeNo worries, happens to the best of us.
Comment #32
quietone commentedSetting this back to NW to remove 'Please'. I have offered a suggestion for the changed text.
Comment #33
quietone commentedI updated the CR as well, someone should check my work.
Comment #34
kristiaanvandeneyndeSetting back to NR and then RTBC if things still go green as this was merely a text change. Change record changes look fine be me.
Comment #35
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #36
quietone commentedShould be NR.
Comment #37
kristiaanvandeneyndeSetting back to RTBC as per #34
Comment #40
catchThe needs review bot in #35 was correct, however I was able to get the MR diff to apply with patch -p1. This looks good to me and will enable improvements in contrib.
Committed/pushed to 11.x (10.2.x), thanks!
Comment #41
andypostCR published https://www.drupal.org/node/3348054 - set branch to 11.x but release to 10.2.0
Comment #42
kristiaanvandeneyndeAwesome, thanks!
Comment #44
liam morlandThis has caused #3410098: Deprecate passing non-strings to UserSession::hasPermission() and User::hasPermission().