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

  1. Move the code from User(Session)::hasPermission() to a new service and that's it.
  2. 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

CommentFileSizeAuthor
#35 3347873-nr-bot.txt90 bytesneeds-review-queue-bot

Issue fork drupal-3347873

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

kristiaanvandeneynde created an issue. See original summary.

kristiaanvandeneynde’s picture

Issue summary: View changes

kristiaanvandeneynde’s picture

Issue summary: View changes
Status: Active » Needs review

Proof of concept added

andypost’s picture

andypost’s picture

smustgrave’s picture

This 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.

borisson_’s picture

Wasn't sure if this will need special tests or if the current tests work.

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.

kristiaanvandeneynde’s picture

Regarding 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.

kristiaanvandeneynde’s picture

Another 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.

smustgrave’s picture

Issue tags: +Needs change record

Understood on the tests.

Still think it will need a change record as a new service is being added.

kristiaanvandeneynde’s picture

Didn't @borisson_ already add that? Or does the tag need to stay either way?

borisson_’s picture

Issue tags: -Needs change record

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.

smustgrave’s picture

Think 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.

kingdutch’s picture

This 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 hasPermission calls and this issue would make that possible.

We eventually implemented a workaround in the simple_oauth module 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 PermissionChecker name 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 to RoleBasedPermissionChecker which 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 RoleBasedPermissionChecker doesn't sufficiently cover the class' behavior then we could also go with DefaultPermissionChecker which would also communicate it's a Drupal default and could be swapped out.

kingdutch’s picture

I 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) :)

kristiaanvandeneynde’s picture

Re #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.

kristiaanvandeneynde’s picture

Updated the CR with example code as well as I could :D

catch’s picture

Status: Needs review » Needs work
Issue tags: -Needs framework manager review

This 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.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Added the requested information to the interface.

kingdutch’s picture

Status: Needs review » Reviewed & tested by the community

I'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

bramdriesen’s picture

One 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.

kristiaanvandeneynde’s picture

Re #22, no it does not:

  /**
   * {@inheritdoc}
   */
  public function isPermissionInRoles($permission, array $rids) {
    foreach ($this->loadMultiple($rids) as $role) {
      /** @var \Drupal\user\RoleInterface $role */
      if ($role->hasPermission($permission)) {
        return TRUE;
      }
    }

    return FALSE;
  }

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.

bramdriesen’s picture

public function hasPermission($permission) {
  if ($this
    ->isAdmin()) {
    return TRUE;
  }
  return in_array($permission, $this->permissions);
}

Which is used by isPermissionInRoles does 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.

kristiaanvandeneynde’s picture

Status: Reviewed & tested by the community » Needs review

No, that isAdmin() check is to see if the role is flagged as admin. Anyone could have that role, it's not limited to UID1.

kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community
bramdriesen’s picture

I 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.

bramdriesen’s picture

Status: Reviewed & tested by the community » Needs work
kristiaanvandeneynde’s picture

Status: Needs work » Reviewed & tested by the community

With all due respect, Bram, but you are wrong here.

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.

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.

bramdriesen’s picture

🤦‍♂️ Think I was sleeping haha. You are indeed correct.

In that case RTBC+++!

kristiaanvandeneynde’s picture

No worries, happens to the best of us.

quietone’s picture

Status: Reviewed & tested by the community » Needs work

Setting this back to NW to remove 'Please'. I have offered a suggestion for the changed text.

quietone’s picture

I updated the CR as well, someone should check my work.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Setting 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.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The 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.

quietone’s picture

Status: Needs work » Needs review

Should be NR.

kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community

Setting back to RTBC as per #34

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

  • catch committed 085ae2c2 on 11.x
    Issue #3347873 by kristiaanvandeneynde, BramDriesen, smustgrave,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

The 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!

andypost’s picture

CR published https://www.drupal.org/node/3348054 - set branch to 11.x but release to 10.2.0

kristiaanvandeneynde’s picture

Awesome, thanks!

Status: Fixed » Closed (fixed)

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

liam morland’s picture