Background
In D8 the default access settings are simple: there is a single 'administer users' permission that controls editing other users.
More complex schemes are possible using contrib modules to allow selective editing of other users under certain conditions. An example of such a module is Administer Users by Role. I am the maintainer of this module, trying to port it to D8.
Problem
contrib modules cannot grant field-level access to some fields on the user. For example, in a view of users, the "status" column will not be present except to a user with 'administer users' permission.
- The code in the contrib module can implement hook_ENTITY_TYPE_access() for entity type "user" which works to allow editing of the user.
- The code in the contrib module can implement hook_entity_field_access, but this doesn't work because the default code in UserAccessControlHandler has set AccessResult::forbidden().
The documentation here states
Note: Even a single module returning an AccessResultInterface object from hook_node_access() whose isForbidden() method equals TRUE will block access to the node. Therefore, implementers should take care to not deny access unless they really intend to. Unless a module wishes to actively forbid access it should return an AccessResultInterface object whose isAllowed() nor isForbidden() methods return TRUE, to allow other modules or the node_access table to control access.
Solution
UserAccessControlHandler should set AccessResult::neutral. This prevents access by default, but allows other modules to override.
The password field is an exception because it should be forbidden for any user to read it under any circumstances.
Patch coming.
| Comment | File | Size | Author |
|---|---|---|---|
| #84 | user-access.2773645-84.patch | 18.52 KB | adamps |
| #75 | user-access.2773645-interdiff-73-74.txt | 2.31 KB | adamps |
| #75 | user-access.2773645-74.patch | 18.52 KB | adamps |
| #73 | user-access.2773645-73.patch | 17.81 KB | adamps |
| #56 | user-access.2773645-56.patch | 17.81 KB | adamps |
Comments
Comment #2
adamps commentedComment #3
adamps commentedFixed patch
Comment #6
adamps commentedComment #7
yogeshmpawarTesting same patch against 8.3.x
Comment #8
vtcore commentedThe patch works fine for me on 8.2.x and 8.3.x. I would very much like to see it committed in 8.3.x as it's a very important fix for access control.
Comment #9
xjmThanks for proposing this patch.
To consider this change for inclusion in Drupal 8, we need to add some automated test coverage.
Also tagging for a subsystem maintainer review since this is potentially a risky change that might result in information disclosure where a site or module was previously relying on the access being forbidden when combined with some other access result. We'll need to consider it carefully.
Thanks!
Comment #10
adamps commented@xjm Thank you for looking at this issue.
I understand the need to add tests. However unfortunately I likely won't have time to do it myself in the next few months. I hope that someone else interested in Administer Users by Role for D8 might help out. I have put a request for help on the D8 porting issue.
Comment #12
nyariv commentedCommented on wrong issue.Comment #13
Xilis commentedI've added a patch that includes the patch #7 and a test.
The test includes a view of users and some fields (name, status, mail, init, changed), and checks whether the fields (columns) are present. It includes a hook_entity_field_access() for the mentioned fields.
Before adding the patch, the view only shows the name and created columns, after the patch they're all visible.
I have included screenshots of the view and test results before/after patch (the view was imported for the screenshots).
View before patch:




View after patch:
Test results before patch:
Test results after patch:
Comment #14
adamps commentedMany thanks for the tests @Xilis
I think next step is "subsystem maintainer review". If @xjm or anyone else can help the subsystem maintainer to see this issue that would be much appreciated.
Comment #15
yogeshmpawarAny Update on this issue ?
Comment #16
cilefen commentedIt needs a review done.
Comment #17
adamps commentedClarification: this has been reviewed and tested by the community. It needs "subsystem maintainer review".
Comment #18
dinesh18 commentedI have reviewed the patch #13 and found the below status :
5 passes, 10 fails, 0 exceptions, 2 debug messages. PFA screengrab for the tests.
Also, after applying the patch I can see two checkboxes for user in Testing .PFA screengrab.
Seems to be something wrong or help me if i did some mistake.
Thanks.
Comment #19
adamps commented@Dinesh18 Thanks for your review.
Tests: the official test-bot ran successfully on #13 last night.
Repeated user: I don't see that problem and the screenshots in #13 are also fine. I think other websites are using the patch successfully. Perhaps the reason you are seeing this is related to the problem you have with tests? If you think there is a bug with the patch, please can you describe a sequence of steps that shows the problem?
Comment #20
dinesh18 commented@AdamPS,
Below are the steps which shows the problem :
1) Install Drupal 8.4.x-dev
2) Enable the Testing Module
3) Apply the patch #13
4) Run the test UserFieldAccessChangeTest
Let me know if I have done something wrong.
Thanks.
Comment #21
adamps commented@Dinesh I'm not sure what your aim is here. If you are using this patch in a particular scenario and have a problem, then please explain the scenario with enough information for other people to reproduce it.
If you are trying to run the tests for yourself, then note that as I mentioned in #19, the tests are running successfully overnight on Drupal.org. If your local tests are failing then it seems most likely you have done something, wrong (and that problem could also explain your duplicate user). The fix has been reviewed and tested by the community, and we are now waiting for subsystem maintainer review.
Comment #22
wim leersI agree that changing these from "forbidden" to "neutral" makes sense: it allows contrib/custom code to add new permissions for example, to still allow these things to become allowed.
"forbidden" should only be used for cases where a hard "NOPE NOT ALLOWED" is necessary. For example, in
\Drupal\Core\Access\CsrfAccessCheck::access().I think it'd be better to show a realistic use case: allow access to these in case the user has a certain permission.
Also:
===instead of==.And:
would be much more elegant.
NW for point 2, and for writing a change record.
Comment #23
wim leers(Although in theory, this is only a soft blocker, because it's possible to override
\Drupal\user\UserAccessControlHandlercompletely, but that'd be far more dangerous of course.)Comment #24
wim leersSummarizing #22 + #23:
Once #22.2 is addressed, this can go back to RTBC.
Comment #25
berdirthere was recently a support question and we figured out that it is possible when using the alter hook, but that's complicated.
field access is a common problem because the default is allowed, unlike entity access, so you usually have to use forbidden in hook implementations and many of our helper methods don't work (there is no allowedIfPermission() counterpart, forbiddenIf doesn't work)
Comment #26
adamps commentedThank you for the feedback. I have written a draft change record which needs review please. It is my first time to write a CR so apologies for any mistakes.
I have set to "Needs review" for the CR (following this procedure). However it is also "Needs work" for the work required on the tests, so should go back to that state after the CR has been reviewed.
@Xilis if you are able to update your test code that would be very helpful.
Comment #27
wim leersYour change record looked good! :) I made only minor changes. Well done!
#22.2 still needs to be addressed, so back to NW.
Thanks!
Comment #28
dinesh18 commentedHere is an updated patch which implemented #22.2 and interdiff
Comment #30
dinesh18 commentedHere is an updated patch which implemented #22.2 and interdiff.
Changing the status to Needs Review
Comment #31
berdirshould use short array syntax.
Since this was now changed to use an in_array() check, you now have 3x the same code. That means the second and third if are not needed and you can remove it.
In fact, you could even do a return AccessResult::allowedIf($operation == 'view' && in_array(...)) and shorten it all to a single line.
Comment #32
dinesh18 commentedHere is an updated patch and interdiff.txt implements #31.
Comment #33
pcambraI think the feedback from #22.2 is not really being addressed in those changes, what Wim probably means is that a more natural usage of this checks is permission based, something like this:
Instead of
Comment #34
wim leersNo, I meant
Comment #35
berdirThe short array and coding style on the line above is not fixed yet, and there's a question of using local variables or not (so leaving needs work for that), but that's what we have now, except that it is an allowedIf()?
Comment #36
dinesh18 commentedIt seems allowedIf() is better to use because it creates an allowed or neutral access result. Here is the final code with allowedIf().
return AccessResult::allowedIf($operation === 'view' && in_array($field_definition->getName(), ['status', 'init', 'mail']));Kindly review it so that I can create the patch for it.
Comment #37
wim leersOh, so it is already implemented! Sorry, I missed that.
Re-reviewing…
Why is this installing
views_ui?This should:
user_access_testComment #39
adamps commentedNew patch
Comment #40
adamps commentedComment #41
adamps commentedIt feels like this patch could be ready to go. Please could somebody check the changes from #39 and then set RTBC?
It would be great if one of the core committers had time to take a look.
Comment #42
DolfAndringa commentedHi all, do I understand correctly that even with this patch, you still need to create your own module to override access permissions to the fields? So the example of #13 was including a separate module that implements hook_entity_field_access to allow access to those fields in the screenshots? Or is there a way with this patch to directly display the email address in a view without additional module development?
Comment #43
DolfAndringa commentedOK, I applied the patch and implemented hook_entity_field_access in a custom module that assumes field_permissions module is installed. It's a tiny custom module, and it works like a charm with patch #39 applied.
I reported this also to the field_permissions module so hopefully the functionality can be included there directly.
This is my module for anyone who runs into the same issue:
Comment #44
agoradesign commented+1 for RTBC
Comment #46
adamps commented@agoradesign thanks - I wonder if you could actually set the status to RTBC as this might trigger the core commiters to come back and take a look.
Comment #47
agoradesign commentedGood idea.. I'm using this patch already for a couple of weeks on a production site
Comment #49
adamps commentedFix coding standards
Comment #51
adamps commentedSorry git muddle try again
Comment #53
adamps commentedFix patch to account for modified tests directory structure in 8.5.x
Comment #54
adamps commentedComment #56
adamps commentedSorry for so many attempts hopefully I've got it this time
Comment #57
adamps commented@agoradesign You should be able to set this one back to RTBC now thanks
Comment #58
agoradesign commentedstill works and looks good :)
Comment #60
adamps commentedTest failure was just a glitch - resetting to RTBC, thanks @agoradesign
Comment #61
wim leersThis if/else statement only exists because
allowedIf()returns "allowed" or "neutral", but we needed "forbidden" to do exactly the same as what was there before.If we're changing this from "forbidden" to "neutral", we can remove the if/else.
Similarly here, can become:
Same here:
return AccessResult::allowedIf($operation == 'view');Comment #62
adamps commented@Wim Leers well spotted - done
Comment #64
adamps commented@Wim Leers OK, so I think what is happening is that the old code was caching the allowed but not caching the forbidden. The new code is caching both and causing test failures.
I don't believe it was your intention to actually change the behaviour, so I have hidden the new patch. I am reluctant to let this issue be diverted into changing access/caching that is not related to the original task, and may cause security/back-compatibility concerns (which would also be in an area not easy for me given my current level of understanding and available time).
If I raise a follow-up issue to consider the matter please can we reset it to RTBC?
Comment #65
berdirIt's not about being cached or not, at least not the first test fail but by what it is cached.
Before, it was only cached by user if you had access. Now always.
The new behavior is actually correct, but it's the same problem as elsewhere.. per-user/owner checks are practically uncacheable. But that's not really related to neutral/forbidden, so I guess we could move that to a separate issue.
Here's an interesting thing, though. I was actually confused why this worked, because as I mentioned before, field access is different to entity access because it defaults to allowed. That means to deny access in hook_entity_field_access(), you *must* use forbidden, neutral doesn't work. But as the test added here shows, it does indeed work in the checkFieldAccess() method. The reason for that is that unlike access(), fieldAccess() does this:
> $default = $default->andIf($entity_default);
Any other check is done with an orIf(), meaning the logic cited in the issue summary is TRUE. However, andIf() is different, it means both access checks must explicitly be allowed, not neutral. That's why this works. Considering how field access works, I think we should have done the same with the field access hooks, but then we would also need to implement and document them accordingly (that they must return allowed by default, not neutral). So this is not a change we can make in a BC-compatible way, ever. I also still think that we should allow empty responses instead of neutral and filter them out. Field access checks are a considerably performance issue and because it is a global hook, any hook implementation for any entity type and field always returns a result that needs to be merged together.
Comment #66
adamps commentedThanks @Berdir. I have raised #2941966: UserAccessControlHandler should use allowedIf for the other part which is non-urgent tidy up.
On the other hand, this issue is a contrib blocker, so it's great to be able to move it along without being delayed.
Based on @agoradesign RTBC in #58 and your comment in #65 agreeing to split off #61, I think we are legitimately RTBC again on #56, so I've set that. If anyone disagrees, please set back and explain why.
Comment #67
berdirFine with me, although I don't really agree with it being a blocker. as commented already in #25, there is an alter hook, using that is a bit more complicated, but it definitely allows to do whatever you want, including replacing the default access check completely.
Comment #68
adamps commented@Berdir Thanks. I guess in cases like this there is a grey area - a workaround exists, but each contrib/custom developer working in the area will take time to figure out the answer. At the moment, it feels like my module is more workaround than code:-) Anyway I understand if you want to remove "Contributed project blocker".
Comment #69
adamps commentedThis issue is also a blocker for #2854252: User forms broken for admin without 'administer users' which is a Contributed project blocker with a much more complicated/hacky workaround.
Comment #70
adamps commentedIn this comment on the related issue, @xjm said:
Doing the same here.
@alexpott if that's you then thank you again for your time. This issue is similar to #2854252: User forms broken for admin without 'administer users' which you looked at late last year. In fact the patch on the other issue includes code from here because there is a dependency. This issue is hopefully much simpler and safer than the other one, so it would be great to get it committed, thanks.
Comment #72
adamps commentedThe testbot is testing the wrong patch. I've changed the order, try again.
Comment #73
adamps commentedI'm baffled, the test passes locally for me. Try uploading the same patch with a new name in case that resolves the confusion.
Comment #75
adamps commentedOK here is a new patch that should work.
I'm not entirely sure why the old patch stopped working. However the new patch is better as the test module more closely resembles what a real module would do.
Comment #76
adamps commentedIt would be great to get this long-standing issue resolved and unblock some other issues.
Please can someone review and set RTBC? It should take less than 5 minutes, and I have provided an interdiff. I have only changed tests so you don't need to manually test again.
Please can anyone help get a subsystem maintainer/framework manager review? @Berdir please could you assign this issue to the appropriate person?
Comment #77
aludescher commentedI tested patch user-access.2773645-74.patch from #75 on Drupal 8.5.1 and confirm that it works.
Comment #78
hi_ten_ja commentedComment #79
adamps commented@aludescher Great thanks - please can you set the status field to "Reviewed and Tested by the Community"
Patch in #78 has no comment to explain, is missing tests and incorrectly exposes the password field so hiding it.
Comment #80
adamps commentedSetting RTBC based on #77 and other community reviews of earlier versions.
Comment #81
wim leersHow can a 1.38 KB patch without tests be RTBC if we previously had a 18 KB patch that included test coverage (in #62)?
Comment #82
adamps commented@Wim Leers thanks for coming back to this issue.
The RTBC is intended for #75.
#78 seems to be a distraction coming with no comment to explain it, no tests and some obvious flaws. I have hidden #78 and moved it low down the list of patches. I don't have the permission to remove the #78 patch entirely - if you do please do so.
I guess I could load up #75 again as #83 if that would make it clearer.
Comment #83
wim leersOh …
Yes, please do! Thanks :)
I'll re-RTBC once you've done that.
Comment #84
adamps commentedThanks @Wim Leers
Here is #84 that is identical to #74
Comment #85
wim leersComment #87
adamps commentedReset to RTBC - just another testbot glitch
@Wim Leers do you know who else needs to review this? If you do, please could assign them?
Comment #88
catchThis is asking for a subsystem maintainer review, but while Berdir isn't listed, I think he counts enough. Moshe to my knowledge hasn't worked on user module for a while.
#65 is an unfortunate discrepancy, we should consider a 9.x issue to see if it's something we can change in a way that while it breaks bc, allows modules to account for both the old and new behaviour maybe?
Committed e7f061d and pushed to 8.6.x. Thanks!
Comment #90
adamps commentedFantastic, thanks @catch.
Re #65, there is already a separate issue #2941966: UserAccessControlHandler should use allowedIf. I have added a patch and the issue is now "Needs tests". Possibly it can be fixed in D8 even, but I don't understand it well enough myself to say.
This has unblocked #2854252: User forms broken for admin without 'administer users' which is on a related theme. Berdir, catch or anyone else I would be hugely grateful for any way you can help over there. The issue received several largely favorable reviews from Alex Pott, but we didn't quite solve everything before he moved on. It has been RTBC by several other members of the community. Following a recent minor improvement the issue is currently "Needs review", but we could say it is 99% RTBC:-)