Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
Following on from http://drupal.org/node/1945390#comment-7247250 with further detail in http://drupal.org/node/1945390#comment-7215306, we ideally want user_access to be an injectable service.
Proposed resolution
Convert user_access into a service that can be injected wherever needed.
Remaining tasks
Experimental conversion- Initial sanity check
- Finalize conversion
- Convert all instances in code to use the new service, including test conversion
- Reviews (tests should be covered by existing tests using the new service)
- Documentation updates
User interface changes
None
API changes
All instances of user_access would be changed to the new service.
Related Issues
#1945390: Convert book_admin_edit to a new-style FormInterface implementation
Follow-ups
#2045361: Mark user_access as deprecated
#2048171: [meta] Replace user_access() calls with $account->hasPermission() wherever possible.
Comment | File | Size | Author |
---|---|---|---|
#88 | user_access-1966334-88.patch | 13.25 KB | dawehner |
#88 | interdiff.txt | 2.02 KB | dawehner |
#86 | drupal-1966334-86.patch | 13.22 KB | ParisLiakos |
#80 | drupal-1966334-80.patch | 14.93 KB | ParisLiakos |
#80 | interdiff.txt | 871 bytes | ParisLiakos |
Comments
Comment #1
Alan Evans CreditAttribution: Alan Evans commentedInitial stab at converting user_access into a service - I'd like to get some early feedback or sanity checking on this if possible before working on it further. Would be good to get an idea if the direction is reasonable before starting on converting all calls and tests.
Comment #3
Alan Evans CreditAttribution: Alan Evans commented... of course it's usually helpful to add all the right files to the patch ... trying again ...
Comment #5
Alan Evans CreditAttribution: Alan Evans commentedNot sure what's going on with the tests (all green locally, and impact should be limited) ... rerolled with a whitespace fix
Comment #7
Alan Evans CreditAttribution: Alan Evans commentedpuzzling results ...
Comment #8
Alan Evans CreditAttribution: Alan Evans commented#5: drupal-user-access-service-1966334-5.patch queued for re-testing.
Comment #9
Alan Evans CreditAttribution: Alan Evans commentedSilly me ... turns out that any test that alters permissions and then checks with user_access does need converting already, because the drupal_static key changed (and user_access() still uses the old key)
Comment #10
Crell CreditAttribution: Crell commentedThis should be \Drupal::service('user.access') now.
Ibid.
This is right out, because drupal_static() should never be used inside an object.
The default-to-global logic is flawed to begin with. Let's just make $account required. From a service perspective, that's what we want as it makes the object idempotent.
drupal_static() is completely unnecessary inside an object. That's what protected object properties are for.
Blargh. Can we turn that function into a service, too? Or, put it on some existing service? Or, something?
Actually, looking at the function, it should just be a method on this service, and then this service depends on the DB connection. Done. Then we can deprecate both functions.
Comment #11
Alan Evans CreditAttribution: Alan Evans commentedMany thanks for the review, Crell. Will work on a revision shortly.
Comment #12
Alan Evans CreditAttribution: Alan Evans commentedAttaching a messy progress patch with a few test fails for now ... will work on this more ...
Comment #13
Alan Evans CreditAttribution: Alan Evans commentedComment #14
Alan Evans CreditAttribution: Alan Evans commentedAnd as it turns out, if you add a new method, it actually helps if you call it instead of the old func..
Comment #15
Alan Evans CreditAttribution: Alan Evans commentedComment #16
dawehnerNeeds docs.
Afaik we should use $this->container->get('user.access')
Maybe accessService would describe better what this class is about.
Should be {@inheritdoc} now.
I'm wondering whether this should be something provided by user module or whether this is a central core service.
I guess it would make sense to have a method which resets both at the same time. What about just ->reset()?
Needs docs.
Afaik we should use $this->container->get('user.access')
Maybe accessService would describe better what this class is about.
Should be {@inheritdoc} now.
Comment #17
Alan Evans CreditAttribution: Alan Evans commentedSorry, I wasn't looking for a review on this just yet, just a testbot run (hence the "needs work" status).
Thanks for reviewing though - I can still use your comments as a TODO list for cleanup.
There's one task remaining before I start looking at the cleanup (making $account required and checking what breaks), then once both of those are done there would be a lot of conversion left to do.
Comment #18
dawehnerI just hope you try to keep user_access() in this issue (which calls the service then) and people can start and use the service in new code.
Comment #19
Alan Evans CreditAttribution: Alan Evans commentedugh ... sorry, got swamped with work and thought this issue was more blocked than it actually was: I was trying to get the suggestion from #18 into place with no luck at all. Turns out that a full drush cache clear is often not enough to rebuild everything - I had to reinstall my test drupal install to get this working.
Back on course now hopefully, and should have a new patch soon.
Comment #20
Alan Evans CreditAttribution: Alan Evans commentedAttaching a version mainly for the test bot again, just to check that the now-implemented last outstanding suggestion from #10 doesn't have unexpected side effects ($account is now a required argument).
I've also incorporated the suggestion in #18 so user_access remains as a wrapper around the service. The optionality of $account is retained in the procedural wrapper then, but removed in the service.
There are a number of cleanups still to do and outstanding issues from #16. I'm on the fence though about whether this should be a core service: user.module is always required anyway so moving this to a core service is mainly going to come down to a question of where people would prefer it. I'm leaning toward leaving it where it is for the moment, as I think that's where most people would expect to find it. It vaguely seems to "belong" in the user module namespace, but I acknowledge that's entirely subjective.
I'd agree incidentally that both caches seem to consistently need a reset at the same time, so would make sense to combine those.
Not attaching interdiffs until I've got to a point where I'm more or less happy with the basis: I haven't kept local history tidy enough to do that yet.
Comment #21
Alan Evans CreditAttribution: Alan Evans commentedComment #22
Alan Evans CreditAttribution: Alan Evans commentedOK, I'm reasonably happy with this one. I've incorporated all previous review suggestions I think, with the exception of moving the service into a core service - I've left that in the user.module space unless there are strong opinions for making it a core service.
As mentioned previously, I've taken the suggestion of leaving user_access as a still-functional wrapper that can be used. That way we can have a follow-up task to convert all other instances of user_access to use the new service (assuming it gets in of course ...). It's probably not a huge job, but I'd want further opinions on the current patch before converting everything.
Comment #24
Alan Evans CreditAttribution: Alan Evans commentedReverting the RoleStorageController change as $this->container doesn't seem to be set - broke tests.
Comment #25
Crell CreditAttribution: Crell commentedAt least a line break between the properties here. I won't ask you to document the other two since they're unrelated to this patch, but if you want to while you're in there no one will object. :-) (Also, $adminUser, not $admin_user, for object properties.)
This class is a pure service, not a controller. It should not be using ControllerInterface. It should just register with the container as service directly via user.services.yml.
We also need an interface defined for user access checking that this class can implement. That way someone else can swap it out with a MongoDB-based one, or whatever.
This also suggests that the service should also be responsible for collecting and saving user/role data, or at least some service should be. That way you can swap out the entire user permission system. Hm... Not sure if that's scope creep or not.
This should be using the injected DB connection.
Comment #26
Alan Evans CreditAttribution: Alan Evans commentedWell that's just embarrassing ... that was after all the whole point of injecting it. Oh well, I need to be more careful.
Comment #27
Alan Evans CreditAttribution: Alan Evans commentedNeedless to say, I'm not overly keen to add to the scope of this, but I'll have a think about what would need adding to the new interface+class here to support what you were suggesting.
Comment #28
Alan Evans CreditAttribution: Alan Evans commentedSo I think what you'd be suggesting is, aside from the 2 functions that are already included in this UserAccess class (user_role_permissions and user_access), the following would also need inclusion:
Maybe not user_role_load necessarily, which is based on the entity system and has its own swapability built in. However, if the other role functions are included, it only seems to make sense to include it also.
Comment #29
Alan Evans CreditAttribution: Alan Evans commentedConverted user_role_permissions to just be a wrapper.
Comment #30
Crell CreditAttribution: Crell commentedI'm not sure check() is the best name here. It's not really self-descriptive. hasPermission()? Which would then, I think, want to take ($account, $permission), not ($string, $account), so that it naturally reads $account hasPermission $permission.
Specify $param array $roles for completeness.
This should be type hinted to an array.
We should probably also inject this service into at least one of the other services that us currently calling user_access(). Probably the PermissionAccessCheck class is a good candidate. We don't need to all of them, but at least one to prove that it works. :-)
I'm still torn about converting the entire function set. It makes sense, but I worry about scope creep. Of course, with the other functions still hard coded to the database the swappability here doesn't buy us all that much.
Comment #31
Crell CreditAttribution: Crell commentedActually, bejeebus in IRC just pointed me at this issue, which is going to have a radical impact on anything going on here: #1872876: Turn role permission assignments into configuration.. :-(
So, yeah, we should wait for that to go in, and then revisit this issue to move all of the functions into a service.
Comment #32
fubhy CreditAttribution: fubhy commentedI asked whether we can commit #1872876: Turn role permission assignments into configuration. without the last part of cleaning up stale permissions on module uninstallation for now so we can proceed with this patch. If it's a "nay" we should proceed with this. Should be much faster to get this to a clean state than the other patch due to the dependency on #1199946: Disabled modules are broken beyond repair so the "disable" functionality needs to be removed that it would imply in that case. It's not that much work to re-roll the patch over at #1872876: Turn role permission assignments into configuration. anyways.
Comment #33
dawehnerHere is a rerole, which injects the user access service into the permission access checker.
Sadly the tests did not passed due to the following issue: #2015721: Adding a role to a user throws a php error
Comment #34
dawehnerOh I think this was dreditor.
Comment #36
fubhy CreditAttribution: fubhy commentedFixing the fails and integrating with AccountInterface from #1825332: Introduce an AccountInterface to represent the current user.
Wonder if we should move this to Drupal\Core now that we got that AccountInterface? Has to be role agnostic then too though.
Comment #38
fubhy CreditAttribution: fubhy commentedWhoops, forgot a file.
Comment #39
dawehnerShould/could we also call $this->container->get('user.access')->reset()?
Potential follow ups:
Comment #40
fubhy CreditAttribution: fubhy commentedOf course! Missed that. Thanks
Agreed. (2) and (3) are consequentially the same though :)
Comment #41
dawehnerShould have an @inheritdoc
We should make that consistent.
Entity controller can have services injected now.
So we agreed that there should be a follow up for this as well.
Comment #43
fubhy CreditAttribution: fubhy commentedOkay, moved it out again. I like the idea of having a single service for managing all this stuff in one place as #28 suggests but yeah, that's scope creep.
Anyways there's still something wrong in the upgrade pass. Don't have time to look at those fails now.
Comment #45
dawehnerPoint one: Think about patches if they get smaller :) (UserAccess and UserAccessInterface got lost)
For example "BASIC MINIMAL PROFILE UPGRADE, FREE ACCESS" will fail.
Comment #46
dawehnerMh, this fixes some bits but still has small upgrade path fails. You can't create a User object as somehow the container is not ready yet.
Comment #48
dawehnerBerdir has the point that there should be a method on the account interface (hasPermission) instead of having a separate service.
Comment #49
BerdirThe reason for doing it as a method is that it's easier to use (we could still have a proxy method, sure), but mostly that after permissions are also stored in the role config entity, this comes down to the following pseudo-code:
Maybe with a bit caching and stuff, but no database queries involved anymore. Why make a service of this?
Combine it with passing the current user into the access checks as discussed yesterday, the permission and role checks become really simple and nice (apart form the +/, stuff).
Comment #50
Crell CreditAttribution: Crell commentedI agree. Let's just make this a method on the User class and be done with it. No service needed.
Comment #51
Crell CreditAttribution: Crell commentedRetitling.
Comment #52
fubhy CreditAttribution: fubhy commentedYeah.. agreed
Comment #53
BerdirNote that there are still a few places where global user is not yet an AccountInterface, e.g. UpgradePathTestBase. #2017207: [meta] Complete conversion of users to Entity Field API has fixes for that.
Comment #54
fubhy CreditAttribution: fubhy commentedJust playing around a bit... I think we can drop the static user access cache. Even more so once #1872876: Turn role permission assignments into configuration. lands which moves permissions onto the role entity.
I am not sure if we should do more here.. The rest should probably happen in #1872876: Turn role permission assignments into configuration..
One thing that bothers me is that addRole(), etc. currently use $rid instead of a full $role object. We might want to change that.
Comment #55
fubhy CreditAttribution: fubhy commentedComment #57
fubhy CreditAttribution: fubhy commentedForgot a return statement.
Comment #59
dawehnerI'm sorry but it feels really wrong to have the same code twice.
Comment #60
BerdirWe have two quite different classes that implement the same interface, they have to have similar implementations, but it will change more I think. I'm adding a few additional methods to that interface in the issue references in #53, they will have "duplicate "/similar implementations there as well.
I expect they will not be that similar in the end, If we go with msonnabaums idea, we might e.g. add something like a UserOneSession class, that subclasses UserSession and just returns TRUE in hasPermission(). We'll see.
Comment #61
BerdirWow, we have 23 upgrade path tests now ;)
As mentioned before, if you merge in the changes to UpgradePathTestBase from #2017207: [meta] Complete conversion of users to Entity Field API, you should have a green patch.
Comment #62
fubhy CreditAttribution: fubhy commentedChecking if that is true!
Comment #63
fubhy CreditAttribution: fubhy commentedOkay, @berdir and I agreed that we should do some profiling despite the fact that we both also concluded that it probably won't make much of a performance difference as we are really just doing the static caching to cut on function calls (slightly). So if anyone want's to do that today, go ahead... Otherwise I'll get back to it tomorrow.
Comment #64
fubhy CreditAttribution: fubhy commentedRefactoring patch now that #1872876: Turn role permission assignments into configuration. is in.
Comment #65
dawehnerHere is a unit test for both User and UserSession.
Comment #67
fubhy CreditAttribution: fubhy commented#65: user_access-1966334-65.patch queued for re-testing.
Comment #69
dawehnerSo, there is a problem for phpunit/autoloading if you use the simpletest UI, so let's setup stuff different.
Comment #71
dawehnerSo never assume that simple code always work.
Comment #73
dawehner#71: drupal-1966334-71.patch queued for re-testing.
Comment #75
dawehnerAdding tag
Comment #76
dawehnerThe problem with the current patch seems somehow that the global $user is not the one which is logged in the test.
Comment #77
ParisLiakos CreditAttribution: ParisLiakos commentedthis should be green
Comment #78
ParisLiakos CreditAttribution: ParisLiakos commentedtag
Comment #79
dawehnerThis was certainly a good fix.
Maybe something like "// Load roles of the user."?
Comment #80
ParisLiakos CreditAttribution: ParisLiakos commentedyes:)
Comment #82
ParisLiakos CreditAttribution: ParisLiakos commented#80: drupal-1966334-80.patch queued for re-testing.
Comment #83
dawehnerThank you!
Comment #84
fubhy CreditAttribution: fubhy commentedVery good. Thanks. RTBC +1
Comment #85
YesCT CreditAttribution: YesCT commentedThis issue was RTBC and passing tests on July 1, the beginning of API freeze.
Comment #86
ParisLiakos CreditAttribution: ParisLiakos commentedreroll after adding methods to AccountInterface got in
note: size difference is due to exact same changes in
UpgradePathTestBase
happened there:)Comment #88
dawehnerOMG this patch has an error level of over 9000!
Comment #89
fubhy CreditAttribution: fubhy commentedOf course, this is sparta! If we blow up tests, we always reach the 9000. Easy. SPARTA!
Comment #90
ParisLiakos CreditAttribution: ParisLiakos commentedOverall Diff Summary
which is because user_access is a wrapper now, and it is called 48 times:P
Comment #91
ParisLiakos CreditAttribution: ParisLiakos commentedre-rtbcing, i totally missed
load
->loadMultiple
changed thanks dawehner:)Comment #92
Dries CreditAttribution: Dries commentedCommitted to 8.x. Two follow-ups:
1) Can we mark user_access as deprecated?
2) Can you explain what profiling you have done? This patch removes some caching. It does not look to be material due to caching at different levels but it would be good to understand what kind of profiling you did.
Thanks!
Comment #93
xjmSwitching for a followup :)
Comment #94
dawehnerHere is the follow up: #2045361: Mark user_access as deprecated
@fubhy, please speak about the caching.
Comment #95
xjmThanks @dawehner!
Comment #96
fubhy CreditAttribution: fubhy commentedExactly.. So while we removed the direct static caching in user_access() we are still using the values from the $role entities (which are already in memory) and the role assignments of the user entity (also in memory). Instead of directly returning the cached values from user_access() we now go to through both, user entity and role entity, to determine access:
$user->hasPermission() -> $role->hasPermission() (for all roles of a user)
This makes the whole system much more solid and decoupled as it's only bound to the static entity caching instead of multiple, custom caching layers (which you would have to be aware of in order to do proper cache invalidation upon write / delete operations, e.g. when deleting a role). Now, when you remove a user role assignment, or add a new role to the user, or add a new permission to a role, it is automatically reflected in the next $user->hasPermission() check without the need of any cache invalidation (entity system takes care of that).
The only thing that we add are some (very few) additional function calls which obviously doesn't affect performance much.
EDIT: As to the question "what kind of profiling was done" we would have to ask @ParisLiakos.
Hey King Leonidas? Can you give us some detail on the profiling you did?
Comment #97
fubhy CreditAttribution: fubhy commentedUntagging
Comment #98
ParisLiakos CreditAttribution: ParisLiakos commentedI believe you are talking to me:p
it was against the D8 HEAD (at the time) front page. First run was before the patch second run after patch..the only change was 48 more function calls which completely make sense since user_access is now a wrapper..when we have converted everything to call hasPermission() directly there will be no performance difference..that 1kb of ram i didnt even bother to check more
Comment #99
xjmWell, what @effulgentsia pointed out is that the real thing to profile would be a site with a lot of roles, and with individual users with lots of roles. I seriously doubt there would actually be a negative impact from the patch, and overall the removal of the cache can only be a good thing, I think, as @fubhy explains in #96.
Comment #99.0
xjmexperimental conversion done
Comment #100
fubhy CreditAttribution: fubhy commentedAdded a follow-up: #2048171: [meta] Replace user_access() calls with $account->hasPermission() wherever possible.
Comment #101
Crell CreditAttribution: Crell commentedComment #102
dawehnerhttps://drupal.org/node/2049309
Comment #103
jibranFixed some formatting issues, change notice looks good to me. Thanks @dawehner.
Comment #104
agentrickardSample code in the change notice needs to account for cases where you want to check access for a user other than the one making the request.
Comment #105
tim.plunkettUpdated
Comment #106.0
(not verified) CreditAttribution: commentedUpdated issue summary.