This issue only occurs with modules hooks - it has no impact in pure Drupal core.
In Drupal core, there is a single permission 'administer users' that controls whether a user can 'administer' the user account of another user. However the Drupal 8 Entity API makes it possible to refine the access, for example by means of hook_entity_access. This allows modules such as Administer Users by Role to create a "sub-admin" - a user without 'administer users' permissions that nevertheless has access to update some other users' account.
Unfortunately RegisterForm, ProfileForm and UserCancelForm are broken when used by the sub-admin. The problems arise from the fact that each form can be used in two ways: a normal user can update their own account or an admin can administer another user.
Expected behaviour
Tthe sub-admin sees the same interface as the admin, except missing fields such as role that require additionaly access.
Actual behaviour
The sub-admin sees a confusing mixture of the user and admin interfaces, for example:
- After creating a user the sub-admin is logged in as that user.
- When cancelling an account the sub-admin see "Your account will be blocked and ..." but it's actually another users account.
- Several fields are missing due to lack of access.
Steps to reproduce
- Install Administer Users by Role release 8.x-2.0-alpha3.
- Create a role with permissions 'Create new users', 'Edit users with no custom roles', 'Cancel users with no custom roles', 'Access the users overview page', 'View user information'.
- Create user with this role, and log in as that user.
- Create a user.
Or if you prefer, can reproduce by writing custom code.
Workaround
It is possible to fix all the problems using the extensive hooks provided by the user module. However it took 95 lines of code in Administer Users by Role. What's more, this code has detailed dependencies on the internal implementation in the user module, so may need to be updated for future Drupal versions.
You can test the workaround if you also install the sub-module administerusersbyrole_hack. You must disable this sub-module to see the bug.
Solution
Fortunately it seems possible to fix the forms with some fairly simple changes to the user module, without any negative impact on mainline use cases. This seems valuable and worthwhile because any contrib or custom code hooking into User access is likely to see the same problems.
Patch coming up.
Comment | File | Size | Author |
---|---|---|---|
#104 | user.form_.subadmin-2854252-interdiff-92-104.txt | 1.61 KB | AdamPS |
#104 | user.form_.subadmin-2854252-104.patch | 17.74 KB | AdamPS |
#92 | user.form_.subadmin-2854252-92.patch | 17.59 KB | AdamPS |
Comments
Comment #2
AdamPS CreditAttribution: AdamPS commentedDrupal core currently tests for "admin" access by checking for permission 'administer users'.
The uploaded patch instead tests for "admin" access if the active user is not the same as the target account. As far as I can see this method is completely safe because it is based on Drupal core entity access. Drupal ensures that it is only possible to access the form targetting another user when holding the appropriate permissions.
Note that this means that the sub-admin will not see the admin interface when managing their own account. This seems to generate the correct behaviour, for example:
Security implications
Mostly it's the same either with or without the patch:
As written, the patch adds default access for the sub-admin to enable or disable the target account. This seems like the most useful and common default. However if the maintainers prefer I can alter the patch to turn this off by default for sub-admin, leaving the custom/contrib code to override with a hook as required.
Back-compatibility
Next steps
Would appreciate review and feedback from maintainers please.
Comment #3
shawn_smiley CreditAttribution: shawn_smiley at Achieve Internet commentedI tested this on Drupal 8.2.6 and have not experienced any problems.
Comment #4
AdamPS CreditAttribution: AdamPS commentedComment #5
AdamPS CreditAttribution: AdamPS commentedComment #7
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedFix bugs in original patch and update for new Drupal version
Comment #9
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedNow I'm confused. #7 reports that the tests pass, yet #8 reports that they failed. Let's try setting it back to "needs review" in case it was a temporary glitch.
Comment #11
jhedstromThis patch no longer applies.
I think a code comment here would be prudent. It implies that any user editing a user that isn't theirs is an admin (which is probably true), but presumably there is an access check elsewhere that would limit this to actually only be admins?
Comment #12
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@jhedstrom Thanks for taking time to look at this issue.
I will upload a new patch including the comment as you suggest (busy right now - will do in a couple of week's time).
Correct. Although the term "admins" is potentially confusing, and that confusion is partly why this issue exists. I would prefer to say that a user can only edit another user if they have permission to do so. This is enforced by the Drupal 8 Entity system.
Comment #13
agoradesign CreditAttribution: agoradesign commentedRe-rolled #7
Comment #14
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedThanks @agoradesign. If you could set RTBC that would be helpful.
@jhedstrom I have added the comment you suggested
Comment #15
agoradesign CreditAttribution: agoradesign commentedthanks for adding the comment :)
Sorry for being nitpicky, but I saw two double spaces in your comment and removed then (plus optimized line lengths). So I'll leave it to "needs review" then, but for me it's RTBC
Comment #16
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@agoradesign thanks for fixing the comment alignment, which I have reviewed and is fine. You stated that you have reviewed and tested the main fix, so it seems like we are RTBC.
Comment #17
agoradesign CreditAttribution: agoradesign commentedYes, it still works for me :)
Comment #18
xjmThanks for working on this!
We need to add test coverage for the second scenario. (Presumably the first scenario already has test coverage.)
This change looks wrong. I don't think
$admin_access
should be true just because the entity ID doesn't match the user ID. That seems dangerous.This doesn't look right at all...
Comment #19
xjmRather than changing the form internally and (possibly dangerously, or at least confusingly) changing internal flags about whether the user is an admin, should we refactor the form so that the separate code paths are actually separate?
Also, agreed on requiring subsystem maintainer review for this, if possible. That's Moshe currently, although he hasn't really been very active as a core maintainer in a long while.
Comment #20
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@xjm thanks for taking time to look at this issue.
I acknowledge the need for tests, but I don't currently have the knowledge to write them. There is a related issue #2773645: Allow hook_entity_field_access() to grant field-level access to User fields: 'forbidden' -> 'neutral' where writing similar tests is in progress.
#18 points 3 and 4 please could you explain what dangers you see? Perhaps the problem is that I need to add comments similar to the one I added in #14?
Drupal Core enforces that users cannot edit or create other users' accounts without the required permissions. I have researched the relevant lines of code below. However (unless you believe there is a Drupal Core security bug) I didn't actually need to find the lines - I trust core to be secure.
3) UserAccessControlHandler grants access only if the two IDs are equal
4) The route /user/register has requirements _access_user_register (user.routing.yml) which maps to RegisterAccessCheck::access that tests isAnonymous.
The route /admin/people/create requires permission 'administer users' although a contrib module can modify that.
#19 Might it help to add comments and reconsider the variable name? In the new code, $admin means "a semi-privileged user with permissions to administer another user's account" - it doesn't mean "full admin with permission to do anything". Perhaps we could even create a function
isAdminOtherAccount($currentUser, $targetUser)
which would give a single place to write a clear comment.I have little experience of Drupal core dev and would welcome help from anyone else to improve the patch.
Comment #21
agoradesign CreditAttribution: agoradesign commentedI partially agree and disagree (with #19):
I fully agree that the wording is confusing. The "admin" inside the variable names just emphazises that core user management is unfortunately too limited - in terms of either allowing everything or nothing and not taking possible contrib extensions offering more granular permissions into account.
But, whatever the solution in the end looks like, the naming here is unfortunate and wrong (or let's say to limited in scope), as the permission check in these forms is not used for access control of the form itself, but only to certain fields like the existing password.
I thought about if it would make sense to introduce a service that you can ask whether the user "is admin" (== "is a foreign user"), allowing contrib modules to alter the result. However I'm unsure, if this would be satisfying...
Refactoring into separate parts sounds great, but how? Split up the forms into different ones? But as the route is the same, when editing an user by yourself and by another person ("admin"), this seems to be impossible?!
Would be great to hear Moshe's opinion
Comment #22
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedOK, I have changed the variable name and improved a few comments as it sounds like there is generally support for that.
I didn't yet create a function
isManageOtherUser
or do any other refactoring pending the discussion coming to a conclusion.Comment #23
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedComment #25
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedFixed whitespace
Comment #27
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedComment #28
agoradesign CreditAttribution: agoradesign commentedway better! :)
Comment #29
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedSlightly tweaked version
$form['administer_users']
to$form['manage_other_user']
because it could break existing config/custom hooks@agoradesign if you are happy, please can you set RTBC again?
I experimented with changing AccountForm to call EntityAccessControlHandlerInterface::fieldAccess to determine access to fields based on actual permissions rather than hard-coding default core permissions.
I would welcome any thoughts on whether that is a good idea.
Comment #30
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedPatch with some tests but no fix - will fail.
Comment #31
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedSame patch as #29, but with the tests, should pass
Comment #33
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedCoding standards fix of #31
Comment #34
agoradesign CreditAttribution: agoradesign commentedstill works for me
Comment #35
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedComment #36
xjmThanks for the cleanup and the tests; that helps demonstrate what this issue is trying to accomplish.
I still am really not comfortable with this approach. Simply saying that (e.g.) the email field is not required only if the account is not your own hardcodes an assumption about what contrib modules might do. I could create a different module that granted access to only certain fields on the user profile form, and this change would unexpectedly make the email field not required for them, because a different contrib module wants a different behavior.
Why not have the Administer Users by Role module just override the whole user form? That way the module could customize the administrative experience that's appropriate to its permissions and configurations, rather than introducing this change in core. I'd be in favor of a refactor of the user form that made it easier for a different module to subclass it to override some aspects, but changing the core form logic to support logic that doesn't exist in core isn't the best approach IMO.
We still need a subsystem review. Since the user maintainer isn't really active, I'm also tagging for framework manager review since we escalate to the framework managers in cases where a subsystem maintainer doesn't provide feedback.
Thanks!
Comment #37
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@xjm Thanks again for your time.
Yes that is a very good point, and I have been thinking about it too (#29). Actually I think the patch didn't change #access for email, but it did for name, status, password. And in fact I think you're right that email probably shouldn't be accessible by default.
If we look at UserAccessControlHandler, it defines the default access for individual fields. This code seems good (subject to fixing #2773645: Allow hook_entity_field_access() to grant field-level access to User fields: 'forbidden' -> 'neutral'). I propose that AccountForm should match and so my current idea is to call
Repeat that for every field that is not fully accessible by default. FYI This is inspired by #2846365: [regression] User roles field access is inconsistent, users with 'administer users' permission can gain full access which I guess you will be familiar as you have commented there several times. I really like that way that this does away with complex code carefully duplicating the logic in UserAccessControlHandler.
As a contrib module developer, I don't favour that idea. I feel it's easy to get wrong in a way that exposes security bugs and it would work badly in case of more than one module that alters access (only one of them will get its form to run). It seems to partially defeat the point of having an Entity Access system, and takes us back to D7 ways.
My preference is to persevere a little longer with the approach in the current patch. The fact that fields such as email are exposed by default indicates to me that this form is in need of some revision.
If my preferred approach doesn't work out, this would still be valuable. It would make it easier to implement hook_form_FORM_ID_alter. (As mentioned above I currently don't subclass because that seems to require taking over the route entirely and blocks compatibility with other related modules.)
By the way, I would be very grateful if you would look at the wider issues in #2921112: [META] User access control. Do you think it would be useful to work on them all in a single patch? The more I work on this area as part of developing "Administer Users by Role", the more difficulties I find!
Comment #38
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedHere is a slightly updated patch. The code calls
$account->XXX->access
for 'name' and 'status'. I now believe the patch is good from a security point of view in relation to "sub-admin" users.UserAccessControlHandler in D8.4 (unchanged in patch)
* The last one is perhaps surprising but it seems to be clearly documented and self-consistent throughout the code.
AccountForm in D8.4
Same as UserAccessControlHandler except name is not protected for sub-admin.
After #33 patch
You were right to raise a concern. The status field is not protected and it should be. (Also name is not protected as in D8.4.)
After #38 patch
Same as UserAccessControlHandler.
Comment #39
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedSorry I now see that in #36 you were talking about whether 'mail' is '#required' (I had imagined it as '#access').
I am happy to drop any change to ['mail']['#required']. I guess that would mean that sub-admins were unable to edit or cancel an admin-created user without an email (not a scenario I use personally but I guess some people must). Whichever way we decide, note that there is matching code in \Drupal\user\Plugin\Validation\Constraint\UserMailRequired and I was unable to find an easy way to hook and override that.
In terms of other references to $manage_other_user:
So hopefully I am getting close to something you could be happy with - step-by-step I am eliminating anywhere that has changed the existing behaviour.
Do you think it would be useful if we get together on a chat forum to talk through some issues?
Comment #41
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedNew patch
@xjm many thanks for your time and asking excellent questions to help me improve the patch.
Comment #42
alexpottLooking at this patch from a framework manager perspective. I share @xjm's concerns about just checking that the IDs aren't equal. (Also just checking anonymity in the latest patch I have the same concerns - we should not just be relying on the access checking from the routing system here) However we could just check that the user has edit access to the account entity as well. If it does and the IDs are not equal that it has admin permissions. I'd be tempted to add a trait to forms to determine administrative access. Which has a method like:
That way we can centralise this logic and provide a really good comment in a single location as to what is going on and why it is important. I do agree with what @AdamPS says about the additional security vulnerabilities of making contrib override the entire form. And the direction of the patch to use more entity access in these forms is a good one from the perspective of making them more re-usable and making the security concerns easier to review because all the access is being checked in the same place.
Some review points based on #38 - this was a xpost with #41
I think that the variable name of
$admin
is better than$manage_other_user
since if you have the 'administer users' permission you are not necessarily here managing another user. Where if the user IDs are not equal then you are administering a user.There would then way less change to the form too.
+1 to changing these to field access checks.
As above I wouldn't bother changing the variable name.
Still should have the > 1 check as far as I can see.
Let's just call it $admin - as above the variable name change does not really add anything.
Unnecessary change.
As above - why change the name?
Comment #43
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@alexpott Many thanks for your time.
No action take yet on preamble: "canAdminAccount". I understand the idea but have questions/uncertainties:
1) No action take yet as in #41 I removed $manage_other_user
2) +1
3) +1
4) "Still should have the > 1 check as far as I can see. "
I had expected that
$account->access('delete', $user)
would fail for user 1. It doesn't, but I think it should.For sure I can leave the account ID 1 test in. Alternatively it might be cleaner to fix UserAccessControlHandler as other code might be relying on it. What do you think?
5/6/7) +1
Comment #44
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedcanAdminAccount
On investigation, "canAdminAccount" is tricky. Such a method tries to make a global yes/no admin decision. I can see why you are attracted to the idea, but I'm not sure reality is that simple. Arguably part of the cause of the issues I have been raising is the temptation for Core developers to rely on a single access flag, whereas one purpose of the entity access and permissions system is to break apart the concept of 'admin' into separately controllable flags:
Most of the questions have now been handled because we have converted to calling access() in various places. I think I need two more specific access decisions to complete this patch:
1) Code in RegisterForm/AccountForm to distinguish self-register versus admin-create. How about
$account->access('create')
?2) Code in UserCancelForm to determine access to select account cancellation method. The cancel method 'description' fields all say "Your account..." which means they aren't really usable unless it is the same account. Therefore it seems that
$this->entity->id() != $user->id()
is the way to go.What I can do is keep your Trait idea. I can create wrapper methods to package the logic: UserAccessTrait::isAdminCreate(), UserAccessTrait::canSelectCancellationMethod.
What do you think?
Combining issues
This issue overlaps with several others in the parent META issue. I am finding they overlap, especially in tests, where I would like to make a thorough SubAdminTest that covers all the aspects (and I currently have code in this patch to workaround one of the other issues). How would you feel about having a combined patch for several issues?
Comment #45
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedI'm very grateful for your continued help so that I can try and get the patch right. OK, here's another attempt.
I have applied comments from #42 with the following notes. The patch is much simpler now, thanks @alexpott.
I have now included patches for other closely related issues. Sorry if this is confusing, but the code wasn't working otherwise.
Comment #46
alexpottOne quick thought... More review to come later...
This still does not look correct. See the following:
The $account->id() > 1 check needs to remain.
Comment #47
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@alexpott thanks for your diligence
Did you perhaps run your
drush php
commands without applying the patch? On my server once I have applied the patch it does work:Because I added this:
That code seems like an improvement as it gives an extra layer of protection for any means to delete root user, not just via ProfIleForm. I also confirmed this in the GUI. However if you are still concerned of course I will change it.
Comment #48
alexpott@AdamPS ah I missed that. I agree it adds protection but I don't think we should make that change to UserAccessControlHandler because of #540008: Add a container parameter that can remove the special behavior of UID#1 and just leave the protection in the form for now.
Comment #49
alexpottThis change looks strange. Why are we just checking the perm here?
I don't think this comment needs changing - what it used to say is still true - no?
Do we still have to do the ID comparison - is there no access check we can do?
Why are we making these changes? I don't see how this is related to the scope of the issue?
Comment #50
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedThanks @alexpott
#48 +1 OK, I understand
#49
2) +1 OK
4) +1 Ah that's from #2921114: Username formatter ignores entity access. Sorry, you are right, it's not a dependency of this work and I will take it out of the patch.
The other two points I would like to discuss further please. They are similar to each other: I don't really like hard-coding a specific behavior, but in each case I can't see a satisfactory way to check access. Do you have any ideas? Is it at all feasible to introduce new values of "$operation" specific to the User entity, e.g.
$user->access('choose_cancel_method')
? That would allow Core to define a base behaviour that would be very easy to override in hooks.1) = "Access to set empty email field to empty": on create; to preserve an empty email field on edit/delete. D8.4 checks 'administer users', and the latest patch doesn't change that. @xjm pointed out that any change might be unwanted by other custom modules or existing sites. It's pretty painful to write hooks to overrride the Core defaults: need to hook_form_alter and hook_validation_constraint_alter for UserMailRequired (creating two new classes).
Or perhaps you are asking why I removed the variable $admin? I did that because I feel the key to maintaining the user module in future so that sub-admin access works is to get away from the idea of an all-or-nothing admin. Having a variable called $admin just tempts coders to use it, rather than work out the correct specific access check.
3) = "Access to choose cancellation method". D8.4 checks 'administer users'. I changed that to
($this->entity->id() != $user->id())
. That decision was based on a complication: if a user cannot choose cancellation method, then the description is "Your account..." which doesn't work for a sub-admin. Certainly it would be better to allow a sub-admin who can't choose cancel method.Is it feasible to change these descriptions in a Drupal minor release? I guess it could affect hook_user_cancel_methods_alter, translations, etc.
Comment #51
alexpottForms are not API especially the descriptions. See https://www.drupal.org/core/d8-bc-policy#strings
Comment #52
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedThanks. In fact I think I have just found a neat solution without changing strings:
If "not allowed to select cancel method" and "not cancelling own account" use the $method['title'] instead of $method['description'].
That still leaves the question of how to decide if a user is allowed to select cancel method. Am I allowed to do make up a new operation:
$user->access('select_cancel_method')
?Comment #53
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedNext version ready for review please, covers #48 and #49.
Cancel
Multiple cancel
Mail field required
user_mail_required
to avoid duplication.I will add some more tests including hook_user_mail_required_alter and multiple cancel, but thought I should wait to see if the code is OK first.
Comment #55
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedFix coding standards and broken test
Comment #56
agoradesign CreditAttribution: agoradesign commented@AdamPS is the current 2.0-alpha4 of administerusersbyrole supposed to work with the patch from #55?
I just tried to update from #33 to #55 - and besides the fact that Composer won't patch both this and #2773645-39: Allow hook_entity_field_access() to grant field-level access to User fields: 'forbidden' -> 'neutral', as the same test class file should be created by both patches - administerusersbyrole 2.0 alpha4 stopped working for me... I then returned to patch #33 and it worked again
Comment #58
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@agoradesign Ah good question.
For anyone trying to get a working site (accepting risks of alpha releases and patching core) then it makes sense to stick with 2.0-alpha4 plus #33.
However I'm really hoping that we can get these core patches accepted soon and get the module into a more usable/stable state, and that requires someone to help out with using the latest versions and reporting problems or setting RTBC - if you can great. That would mean module version 8.x-2.x-dev plus the latest patch here, currently #55. I just noticed I had some changes locally not pushed, so there is now a new dev. Let me know any problems that you see.
Yes #55 includes the changes from #2773645: Allow hook_entity_field_access() to grant field-level access to User fields: 'forbidden' -> 'neutral' (this is because there is a dependency to make the tests work on this issue) so drop the other one from your config.
Comment #59
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedActually better if you can give me a few days and I'll tidy up the patch and make some new releases of AUBR.
Comment #60
agoradesign CreditAttribution: agoradesign commentedNo problem. I'm comfortable with the current situation, As the site is already published, I'd hesitate anyway updating between different patches. Looking forward to see this landing in core
Comment #61
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedUpdated patch
Matching AUBR module version coming soon.
Comment #63
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedNewer patch for #2773645 fails tests so going back to previous code
Comment #64
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedAUBR 8.x-2.0-alpha5 ready for testing out user.form_.subadmin-2854252-63.patch.
@agoradesign It would be great if you can help perhaps on a test site. I understand you want to keep your live site "stable", but on the other hand that alpha4 isn't go to be work forever. I really hope we can get a commit in 8.6.x.
Comment #65
BerdirJust to better understand this, the way we solved this is by giving users the administer users permission but then use the userprotect to prevent them from editing admin users and roleassign.module to control which user roles they can assign to existing and new users. (As well as masquerade with some patches to properly limit them to certain roles).
What exactly is the difference to the approach used by administerusersbyrole? Seems kind of like the opposite, userprotect excludes certain roles, this is an opt-in for specific roles? But it could probably be implemented in the same way?
I guess even if we fix core, there are still going to be cases where contrib modules check for that permission to show information on the user edit or view page to only show it to admins. There is for example also the check in the \Drupal\user\Plugin\EntityReferenceSelection\UserSelection that only allows users with that permission to reference blocked users.
Just some random thoughts/input, not saying that we shouldn't do this. also didn't review yet.
Comment #66
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@Berdir You have vastly more Drupal experience than me, but I guess I can call on a long career as a security expert in other fields. Personally speaking I would strongly advise anyone against giving the administer users permission to sub-admins. Sorry, long post, but I think it can help people have safer sites so is important.
Reduce Access strategy
As per #65, this approach is to give sub-admins administer users then try to reduce permissions. AUBR 1.x did that and it was full of security bugs; I took over as maintainer and rewrote as v2. I concede that you could build a secure site if you are a Drupal security expert with detailed knowledge of every module that you add to the site. However it seem to be a "fail-unsafe" approach. You have to identify all the possible ways that a user with "administer users" can escalate access and then shut each one down. If you miss just one, the site is vulnerable.
There is another widely-deployed user/role management module based on "reduce access". I raised a security issue, the vulnerabilities were confirmed, but the issue itself was eventually rejected based on policy (see "What About Vulnerabilities Which Require Advanced Permissions?") and I was given permission to raise the issue in public (which I chose not to do). So in a sense, with this approach the site is no longer protected by Drupal security advisories.
Grant access strategy
On the other hand the AUBR approach to grant access selectively is a "fail-safe" approach. I mean in the sense that if you miss one way that you should have granted access it's not a security bug, it's just a minor nuisance.
Contrib modules
Exactly my point! What if the contrib module adds a new security-critical field to user edit? UserAccessControlHandler::checkFieldAccess will by design grant access to any user with administer users. In the "reduce access" approach the sub-admin has access to the new field and can escalate their own account to full admin.
I expect the example you had in mind is that a contrib module creates a new "semi-secure" field. The sub-admin could in fact safely access it, but with "grant access" by default they won't be able to.
In summary, either strategy might need to write a hook for the new field. However the difference is that in the "reduce access" strategy if you fail to notice you have a critical security issue.
Entity API
Here's another way to look at it: the grant access approach is wholly aligned with the D8 Core entity access API - it uses access hooks to grant access. So interaction with any other contrib module is safe by design provided that the other module also uses the access API correctly. On the other hand with the reduce access strategy, once a user has "administer users" it effectively acts as a bypass of the entity access API for users because of
@ContentEntityType[user][admin_permission] = "administer users"
.Summary
In conclusion, I claim that this issue (and related ones under the parent META issue) are important because they are fixing the D8 API-compliant/safe/recommended way of selectively granting access to users. Currently modules/sites are reacting to these issues by adopting other strategies that are riskier.
Comment #67
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedThanks for pointing that out - yes it looks like that would need fixing too. D8 introduced an amazing entity access API, but lots of code for the user entity ignores it in favour of hard-coded permissions! So far no one has raised it so perhaps it's not that important to people. I don't plan to tackle it right now and I don't really understand the implications enough to raise an issue properly. However I've added it to my personal list of things to be aware of.
It seems that for example
could become something like this
Some of the others are probably harder though.
Impact on the two strategies
I would say that code like this relates to "reduce access" versus "grant access" on a case by cases basis
Comment #68
BerdirThe "amazing entity access API" has holes, unfortunately :). I specifically meant buildEntityQuery(). There is no $user in that context, this is a query against all users. And entity access only works on a single entity (with the exception of create, which is a special case).
The second example could possibly be more generic yes, but with operation edit not view :)
Comment #69
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedYes I noticed that:-) I will send you a PM with a specific hole that I think affects security of the site you are referring to.
One way of analysing that is to find all the places where there is a hard-coded permission of administer users. I guess what I am trying to say is:
Specifically for buildEntityQuery my understanding is:
What we are trying to do with this issue and similar ones is to reduce some of the holes in the "amazing entity access API" which I think benefits everyone whichever strategy they pick.
Comment #70
BerdirTo be clear, I did not want to say that we should do this. I was just trying to better understand the difference between your module and the ones I mentioned. Didn't know yours was rewritten in that way and your arguments make sense. I think the approach we chose with our sites is fine for our use case but I agree that might not be true for everyone.
Comment #71
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedAnd equally I should be clear that I agree the approach you chose is fine for anyone who has enough time and knowledge to do it right. For example you mentioned the need for "masquerade with some patches to properly limit them to certain roles" and anyone who doesn't understand that should be warned they are likely out of their depth.
Comment #72
Rob230 CreditAttribution: Rob230 commentedNone of the patches I tried from this thread would apply for me. Maybe because I'm on 8.3.x. Is there any way you could reroll for 8.3? Or tell me which patch to use to get the administerusersbyrole module to work?
Comment #73
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@Rob230 Thanks for trying out the module and patch.
8.3 is old, 8.4 is current, 8.5 is nearly ready.
If you want to try out new modules that rely on core patches then I think it's essential to be on as new a Core version as possible.
Comment #74
AppLEaDaY CreditAttribution: AppLEaDaY as a volunteer commentedI applied the patch to a 8.5.0 core and it made his job. I've been told 8.5.0 and 8.6.x-dev are the same in the areas related to the patch, so my experience is however representative.
Comment #75
Rob230 CreditAttribution: Rob230 commentedI have been using patch #63 on Drupal 8.5 and not experienced any problems.
Comment #77
MixologicRe-marking RTBC as the testbot fail was in error.
Comment #78
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedNew patch with a minor enhancement.
Allow modules to customise whether the user mail field is required. By default in core, this is controlled by 'administer users' permission. This patch allows that to be customised via a hook. The module "Administer Users by Role" version 8.x-2.0-alpha6 implements that hook by means of a new permission 'allow empty user mail'.
Please can someone do a quick review and set back to RTBC?
Comment #79
borisson_I've tried this patch in conjuction with #2949408: Allow group admins to create user account and add to group for groups and it's not giving me what I expect from this patch. I'm probably misinterpreting the problem here but the problem I'm having is that the status and roles fields are not visible. If that is not the problem that this patch is trying to solve.
Using that patch made me question some of the documentation in this one though, there is documentation (in multiple places) that talks about routes for admin creation and self registration. In my case I'm using a groups url to create the users (
group/1/content/create/group_membership
).I think that means that we should improve the documentation about the difference between admin creation and self registration and probably only mention it once (and use an
@see
to link to the other documentation).Comment #80
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@borisson_ thanks for trying this patch.
This patch mostly tries to take whatever access has been set on a site and make that access work correctly.
The patch does not force status and roles fields to be visible because that is not necessarily what is wanted on all sites. In particular, the roles field is not generally safe to expose because a sub-admin or group-admin could edit their own account to gain an admin role.
To alter field access a module should implement hook_entity_field_access - for example see administerusersbyrole_entity_field_access.
I am open to trying to help with that on a small scale. However this is an issue with a primary goal to fix a bug so I don't necessarily think it makes sense to get sucked into a major documentation project.
Can you make a specific proposal please? Ideally that would be in the form of a patch.
Comment #81
borisson_I don't have time, right now, to roll a patch, but this is my proposal.
There are 2 usecases when this form is viewed:
- A user creates their own, new, account.
- An administrator creates a new account for another user.
Only when the current user is logged in, and they can create users, it can only be the second case.
Comment #82
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedHow about this:
Comment #83
borisson_Yeah, the thing I'm not sure about is mentioning the routes there, it's easy enough to display the form on another route.
Otherwise this looks good.
Comment #84
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedTrue but almost anything in Drupal can be customised so I would think many of the comments in Core might no longer apply with enough hooks. I'm trying to give an extra bit of information to help people understand what the two cases are. Now I think about it, I feel it's probably clearer for most people to put paths rather than routes - how about this?
Comment #85
borisson_Sure, that makes more sense. I understand your hesitation and I think that this is a good compromise.
Comment #86
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedOK, here is a patch as per #84. Only comment changes compared with #78. I would be very grateful for a RTBC please.
Comment #87
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedReroll patch now that #2773645: Allow hook_entity_field_access() to grant field-level access to User fields: 'forbidden' -> 'neutral' has been fixed.
Comment #89
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedSorry merge error try again
Comment #91
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedOK please can I try and revive this issue which seems pretty to close to ready but has stalled and now missed 8.6.
Removing tags "Needs subsystem maintainer review, Needs framework manager review" as AlexPott has reviewed most of the patch. The main parts he didn't review are
Setting back to RTBC. Many reviewers have reviewed different levels of the patch, except item (2) in the above list - which I could remove from the patch if necessary.
It would be great to get some feedback from the core committers please, many thanks.
Comment #92
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedOK, I have learnt from other core issues I am working on that it is required to keep each issue as small as possible and not to merge separate changes together.
Here is a new patch without user_mail_required so same as #63 plus the reroll to get the tests working and the improved comment from @borisson_. Apologies for going round in circles; I will raise a separate issue for user_mail_required once this one has been committed.
Comment #93
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedSetting to RTBC because #63 was reviewed and this is the same apart from reroll and comment.
AlexPott has reviewed most of the patch apart from UserCancelForm.php how to control permission for sub-admin to select cancellation method using existing permission 'select account cancellation method'.
Comment #94
a.milkovskyI can confirm, that the #86 patch works well with Drupal 8.5.6.
Patches #87 and #92 require Drupal 8.6, have not tried them.
Comment #95
alexpottI've read through \Drupal\Core\Entity\EntityAccessControlHandler::createAccess and \Drupal\Core\Entity\EntityAccessControlHandler::checkCreateAccess and think that with core this will always end up checking the
administer users
. It would be good if a security expert confirmed. I have a slight concern that changing this behaviour might expose bugs if someone has incorrectly implementedhook_entity_create_access()
BUT if they have done that then they probably have other problems.On first reading this change makes me uncomfortable as it looks like we're giving permission for the anonymous name to change but looking at \Drupal\user\UserAccessControlHandler::checkAccess() it does
So the code we're changing is super weird because why does a user you're in the process of creating return TRUE for
$items->getEntity()->isAnonymous()
- but this is not the fault of this patch. I thinkisNew()
would be a better check here. Let's open a follow-up for that.Comment #96
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@alexpott thanks again for your time and expertise.
Set to needs review for the CR and perhaps a security review as per 1).
Comment #97
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedI raised a separate issue #2992848: Allow customisation of permission to create user with blank email for the hook_user_mail_required_alter part that was present in some of the earlier patches.
Comment #98
joachim CreditAttribution: joachim as a volunteer commentedWhat do we need a CR for? https://www.drupal.org/core/d8-bc-policy says that the structure of forms is internal.
Are we sure that's not a functionality change? UserAccessControlHandler::checkFieldAccess() seems to not allow admins to change the username.
Comment #99
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@joachim thanks for your time
I guess that is primarily a question for alexpott. Not sure if you read what I put in the CR - perhaps you could and then say whether you think each part of it is necessary or not?
I think you missed the code at the top underneath the comment
Administrative users are allowed to edit and view all fields.
Comment #100
BerdirAnything that we change needs to be allowed according to the BC rules. Change records are not for the exceptions, they are to document the changes that are we doing *within* the constraints that the BC policy gives us.
If there is a chance that a change could break someone's site/code, we should do a CR
Comment #101
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedFor clarity setting back to RTBC, because that's the status of the patch, and this issue is ready for the attention of a core committer please.
The CR still needs a review please.
Comment #102
alexpottThanks @AdamPS for the CR it looks good.
These changes to properly use the entity and field access API look great and having the test coverage is also good.
Before I commit it'd be great to get a +1 from @Berdir.
Comment #103
Berdir++ in general, noticed a few things though:
I suppose this could also use $admin_create as there isn't really a reason to limit that capability but also happy to do that later to limit scope.
should be bool.
the uid > 1 check could (should? must?) also be part of the user access control handler? I suppose right now, you could delete UID 1 through REST. (not a security issue because it does require administer users)
strange permission name but that exists already.
could also be part of the existing if condition but it does get rather long then.
Comment #104
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedThanks @alexpott and @Berdir
1. In #36 @xjm raised concerns about altering the behavior of ['email']['#required'] as it would not be BC for existing sites, so I split off a separate issue #2992848: Allow customisation of permission to create user with blank email.
2. Done
3. I suggested the same for the uid>1 check, but see @alexpott response in #47,#48.
4. So I guess we leave it given that it already exists.
5. On balance I think combining the if is more clear, so I did it. It's still a shorter line than the function definition. I also fixed the coding standards error on the comment too long.
Comment #105
BerdirThanks.
On the uid 1 check, not fully convinced by #47/#48, No idea when/if that issue will land and even then, some kind of protection for having at least 1 admin user seems useful, on the other hand, you *can* disable the user, which is enough to lock yourself out of a site (easier to fix though). Either way, no need to block this issue.
Comment #106
alexpottEven though this is a bug fix I'm only going to commit this to 8.7.x because of the security sensitive nature of the code it is touching. We've given careful consideration to security when implementing this but I think the added time to 8.7.0 and the fact this will in the branch so long will be beneficial.
Adding commit credit for reviews that helped the patch and the change record get done.
Committed 6002e2d and pushed to 8.7.x. Thanks!
Comment #108
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedExcellent, many thanks @alexpott and everyone else that helped.
Comment #109
agoradesign CreditAttribution: agoradesign commentedwow, that's really great that we have this committed now :)) Excellent work, especially form Adam
Comment #110
Wim LeersImpressive work, @AdamPS! 😲 👏
Comment #111
LendudeNooooooo!! :)
Filed follow up to move this to BrowserTestBase, #3001644: Convert \Drupal\user\Tests\UserSubAdminTest to BrowserTestBase
Comment #112
a.milkovskyFYI the #104 works with 8.6.1 as well.