Problem/Motivation
In #3452426: Insufficient cacheability information bubbled up by UserAccessControlHandler we fixed the fact that UserAccessControlHandler set the wrong cacheable metadata. A consequence of this is that, if something checks for 'view' access on a user when you do not have the "access user profiles" permission, we need to add the user cache context because of the "is it your own profile" check.
UserThemeHooks::preprocessUsername() uses this access check to determine whether to display the username as a link to the user profile. However, the access check's cacheable metadata is not captured or bubbled from the preprocess method, which can result in a user viewing another user's username (for example, as a node's author) as being linked to the viewed user's profile despite the viewing user not having access to view the profile.
However, capturing cacheable metadata correctly from UserThemeHooks::preprocessUsername() would result in the user cache context from the viewaccess check bubbling and negatively affecting cacheability in the render cache and dynamic page cache. This can be addressed here as well by creating a new entity operation that skips the "is it your own profile" check and showing a link/no link based solely on whether user have the access user profiles permission.
Steps to reproduce
- Install Drupal with Standard profile (
drush si standard -y) - Create two users (e.g.
test1,test2) with theContent editorrole - Log in as one of the content editors (
test1) - Create a new Article node and save
- Observe when viewing the new Article node that the author name ("By
test1") is linked to the author's (test1) profile - Click the author link and confirm the profile is viewable
- Log out
- Log in as the other content editor (
test2) - View the Article node created by the other Content editor
- Observe when viewing the new Article node that the author name ("By
test1") is linked to the author's (test1) profile - Click the author link and confirm the profile page shows access denied
Proposed resolution
- Capture the cache metadata from access checks used in
template_preprocess_username()and make sure it is applied to render array and bubbled when rendered - Add entity operation 'view linked label' specifically to check access for whether all users can link to a user profile (for the use of links to authors of entities). This varies from the 'view' operation for user entities in that there is no exception for users viewing their own profiles. This is to prevent the
usercache context from being bubbled
Apply a short-term fix while we try to move template_preprocess_node() to view displays. (see below under remaining tasks)
Remaining tasks
- Fix the bug in the short term, preferably by making template_preprocess_node properly use the entity reference label formatter, see: https://www.drupal.org/node/2726125
- In the long term, we need to gut or get rid of template_preprocess_node() in favor of #2353867: [META] Expose Title and other base fields in Manage Display
User interface changes
N/A
Introduced terminology
N/A
API changes
TBD
Data model changes
N/A
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #21 | Screenshot from 2025-02-14 11-10-51.png | 33.16 KB | catch |
Issue fork drupal-3506444
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
kristiaanvandeneyndeRelevant code:
Comment #3
catchA label-specific permissions seems like a good idea.
Linking an old issue.
Comment #4
kristiaanvandeneyndeCrediting my colleague Anna as she found this bug and managed to narrow it down to node.module. I took over from there.
Comment #6
kristiaanvandeneyndeComment #7
catchhttps://www.drupal.org/node/2726125 if we used the entity reference label formatter, or equivalent logic, then we'd not be doing these permissions checks at all.
Comment #8
kristiaanvandeneyndeComment #9
kristiaanvandeneyndeComment #11
kristiaanvandeneyndePosted a proof-of-concept that I can confirm on our projects skips the user 'view' access check and trades it in for a 'view label' access check.
Comment #12
godotislateI'm not able to reproduce this issue on HEAD with standard profile install:
Steps taken:
Additionally, in this line in
template_preprocess_node():$variables['author_name'] = \Drupal::service('renderer')->render($variables['elements']['uid']);$variables['elements']['uid']contains a render array with the uid field using the "author" formatter (Drupal\user\Plugin\Field\FieldFormatter\AuthorFormatter), which uses the same "view label" entity access check as the label formatter.Comment #13
berdirDidn't try to reproduce yet, but I can't imagine that we don't have test coverage for this, between dynamic page cache tests, performance tests and so on.
Comment #14
catchI think this is needs more info. The umami node page performance tests should include dynamic page cache hits on a node, not sure if they have the right permissions, and because they're real browser tests they can't check headers so it would be more checking the dynamic page cache hit.
I do think we should try to gut node preprocess though either way.
Comment #15
kristiaanvandeneyndeHmm, this is awkward. We have the same symptoms on two different projects and the MR fixes it. We do use a sort of base profile of our own so let me see if that or any of the modules it ships with interferes with
$variables['elements']['uid']in any way. Have a full day scheduled for this tomorrow so will see what I can find.Comment #16
kristiaanvandeneyndeI can confirm that on a vanilla install it does get called, but the result is discarded. From user.module template_preprocess_username():
As you can see, the access check's cacheable netadata is not used at all. Isn't that a bug on its own? You'd think that whatever cacheable metadata determines user view access needs to be added to the page.
I also confirmed that
$variables['elements']['uid']is the same on a vanilla install and our installs.Still digging why on our sites it does bubble up (or perhaps comes from somewhere else). The rabbit hole goes deeper.
Comment #17
kristiaanvandeneyndeComment #18
catchHad a quick look at user_preprocess_user() and was horrified. Opened #3506680: Remove anonymous commenter support from user module for a start.
On the view profile stuff, I think we might need a 'view linked label' entity access operation, which does everything except the own account check, and then 'view' also includes the own account check or something like that. We should not be linking to people's own profiles in node submitted information when they otherwise wouldn't get a link for anyone else, that's just weird.
Comment #19
kristiaanvandeneyndeOkay so I'm going to be more careful with my statements now because I resent that I cried wolf and it wasn't reproducible on a clean install. I do hope you understand I felt compelled to create a critical issue given the impact if my findings were true.
The bug we found on one project was due to the markerio module adding the user cache context to every single page. See #3506671: Replace using user cache context by lazy loaders to avoid killing the Dynamic Page Cache
The reason I came across core first, was due to how I was debugging. I put XDebug listeners on the Renderer and RefinableCacheableDependencyTrait to track down where the user cache context was being added. This led to me seeing a lot of user cache contexts being merged in (one for every node on the page) and that never happened before. Only after the UserAccessControlHandler fix mentioned in the IS does this happen.
Now, because we are needlessly rendering user names and user names can link to a profile if you have access to it, we basically run a user 'view' access check on every page with a node on it. This leads to the user cache context being added to something that luckily never gets used. But that still feels really dirty.
The bug is very much alive in core, because by all means should that access check actually bubble up. So the only thing saving us here from an uncacheable website is the fact that template_preprocess_username() does a poor job at consuming the cacheable metadata of an access check it ran.
Damn...
Marking as major because of the access checks not bubbling up, but it's no longer critical as one bug cancels out the other. Just not sure how we should proceed here. Feels like a lot of work to rephrase this issue to be about the access bug in template_preprocess_username() so maybe we should open a follow-up but mark it as "please don't fix yet or all hell will break loose" and postpone it until we no longer needlessly render usernames all over.
Also keep in mind that if we fix the access check bubbling up, any page that has author names with links enabled will start varying by user, killing caching once more. So perhaps we need to rethink whether we want to keep that "can access own profile" logic.
Either way, will debug our 2nd project with the same symptoms now. Could very well also be markerio running havoc there.
Comment #20
kristiaanvandeneyndeThis would also be an acceptable fix. But what would we render in that case when you do not have access to view profiles? A label that links to an account but gives a 403 as you click on it?
Comment #21
catchIf you don't have access to view profiles, you get a label that doesn't link, just the display name for the user. Uploading a screenshot from my 11.x sandbox logged out.
I think that the current logic in HEAD means that you would see this for every user, except for yourself - and to me that is a user facing bug as well as a caching issue because there is just no reason to render this differently for the current user - you can get to your own profile from the user menu etc.
If we're eating the cache contexts, then what probably happens is that most of the time, the username doesn't get linked, because someone else views the article first before it gets in render cache, but very occasionally, it does get linked for the current user, and then it would be incorrectly cached as a link, leading to a 403. Didn't try to reproduce this though.
Comment #22
catchComment #23
kristiaanvandeneyndeBy the way, doesn't the needless rendering of usernames mean that we tag said pages with the 'user:ID' cache tag? So if you have an expensive page with 20 nodes on it from different authors and one of them updates their account, your expensive page gets invalidated? Even if you never showed any of their names to begin with?
Comment #24
kristiaanvandeneyndeComment #25
kristiaanvandeneyndeComment #26
catchI didn't check if we're throwing away the cache tags too or not, but yes if we're not that would happen. And if we're throwing away the cache tags then unless something else adds the user as a cacheable dependency then content won't reflect changes to the username.
Comment #27
godotislateJust checked, and when I comment out this line in
template_preprocess_node():$variables['author_name'] = \Drupal::service('renderer')->render($variables['elements']['uid'])The
user:IDtag is not in the X-Drupal-Cache-Tags header.When I restore the line,
user:IDis in the header.Comment #28
godotislateI just looked at
template_preprocess_username()and as mentioned there is no capturing of cache metadata at all. In the part mentioned above:In either condition, the value returned and set to
$variables['profile_access']is a boolean, and not an access object, so there is no cache metadata.As an experiment, I tested this change:
Now when viewing the node, I do see the x-drupal-dynamic-cache header as "UNCACHEABLE (poor cacheability)" as well as "user" in x-drupal-cache-contexts.
I also updated this issue's title from template_preprocess_user() to template_preprocess_username(), but please correct if this is a mistake.
Comment #29
kristiaanvandeneyndeThanks for the confirmations @godotislate!
Keep in mind that if we fix user.module here before we fix node.module in another issue, we actually will introduce the critical issue I reported in the first place.
Comment #31
godotislateI think the fix might be easier than expected, and doesn't need a new entity access operation.
Changing this in template_preprocess_username():
to
should hopefully do all that's needed.
Comment #32
catchI think hard-coding the permission is fine in this case so #31 looks good to me.
Comment #33
godotislateI was wrong about hard-coding the permission. For the "view" operation in
UserAccessControlHandler::checkAccess(), there are also checks whether the user being viewed is active. Adding the new 'view linked label' operation is relatively low lift anyway, so pushed up MR 11216.The IS needs an update, which I can get to later.
Comment #34
kristiaanvandeneyndeI like where this is headed. Have only reviewed the MR on the surface, but the new entity access operation makes sense. Thanks!
Comment #35
mxr576Wow!
Tbh, I am a bit hesitant about introducing a new entity operation, although I am fully understanding why this is feels a compelling solution. Why am I hesitant? Because I have been involved several private conversations about what *view label" is and what not; and I am still not convinced that the majority of devs aware of that entity operation.
Would "view linked label" become a generic entity operation or username only? Should it be handled properly by all logic in core/contrib?
What I can also add that I have an extensive test coverage for username access in my module with a test controller that could be also useful foe debugging.
https://git.drupalcode.org/project/view_usernames/-/blob/1.x/tests/modul...
Comment #37
catchI can't think of a use case for the operation other than for user accounts.
Comment #38
godotislateComment #39
acbramley commentedFound while triaging Node issues, this definitely seems like a User module thing.
I haven't read the full thread but I'm a bit confused why we'd want to show a linked username if the user doesn't have access to view the profile? We'd be showing a link to a 403?
Comment #40
godotislateNo, the username label would not be linked at all. It would just be text.
The effective difference only applies to users who are viewing their own username's label. Access for the "view" operation is allowed for users with the "access user profiles" permission and for users viewing their own profile. Access for the "view linked label" operation is allowed for users with the "access user profiles" permission and ignores whether the user is viewing their own label. This means that the label is displayed as a link to all users, or displayed as text to all users. This removes the need for the
usercache context, and it also makes the label display consistent to all users.Comment #41
godotislateComment #42
catchfwiw #40 sounds like how it should always have worked - there are plenty of ways for people to get to their own account.
Comment #43
godotislateSaw these two issues today that seem to tread similar ground but haven't looked closely:
#3386313: The entity link label formatter should check URL access
#3293287: EntityReferenceLabelFormatter returns a link even if the user only have "view label" access
Comment #44
smustgrave commentedWanted to see what this one needs next?
Comment #45
godotislateI resolved MR threads and rebase to get the tests to run again. I think it's ready.
Comment #46
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #47
godotislateRe-based.
Comment #48
godotislateUpdating title since function was moved.
Comment #50
dcam commentedI rebased this to try and reproduce the issue during my review, but I'm having problems with manual testing. While reading through the issue comments (again) to try and get hints I found this quote from #29:
It sounds like this issue needs to be postponed to deal with the node module bug first. Is that right? Or have things changed? Has another issue been opened to fix the node module bug yet?
I'm also tagging this for an issue summary update because it is next-to-impossible to figure out what this issue is about from it. In particular, the Problem/Motivation section needs to be cleaned up, preferably with a short summary of the problem at hand.
For those reasons I'm setting the issue status to Needs Work. I reviewed the code. It looks good. I couldn't find anything to comment about.
Comment #51
catch@dcam the node issue is #2528314: template_preprocess_node() does not add cacheability metadata but was marked as duplicate of #3458589: Deprecate $variables['page'] for node.html.twig and node_is_page(), however that issue definitely didn't fix the problem mentioned here.
Comment #52
catchThe relevant section in node preprocessNode() is:
That $variables['author_name'] is the problem identified above and it's very much still there.
Comment #53
dcam commented@catch Thank you for clarifying that. I looked at that other issue, but I wasn't sure that it was about the same node problem since it:
A. Is a much older issue than this one, so it couldn't have been opened in response to this
B. Got closed as a duplicate.
Comment #54
godotislateComment #55
godotislateI've updated IS, and it's hopefully more clear.
The issue brought up in #29 is that bubbling cacheable metadata correctly from the preprocess method would bubble the
usercache context, which is very undesirable. We're addressing that here with the introduction of theview linked labeloperation, which is different from theviewoperation, in that we don't care if the viewing user and the viewed user are same. The UI experience will be that all users who can't access user profiles see the user name as static text, all user who can access user profiles see the user name as a link.Comment #56
godotislateJust in case, I also added a CR for the 'view linked label' entity operation: https://www.drupal.org/node/3565758
Comment #57
godotislateOne additional note: While access to the 'view label' operation is granted to all users in
UserAccessControlHandler::checkAccess(), I wonder whether it needs to be accounted for here, in cases where user access control handler is overridden or'view label'access is otherwise modified.Drupal\user\Plugin\Field\FieldFormatter\AuthorFormatter, which uses theusernametheme element, has its own access check against'view label', so maybe it makes sense that the expectation in any use case is that access to view the user name at all is checked before theusernametheme element is added?Comment #58
dcam commented@godotislate, thank you for indulging my request for more and better information. I understand it much better now.
Yes, it is. I don't know where I went wrong with my manual testing previously because I think I did most of the things in the steps to reproduce, but I wasn't getting the cached linked usernames. This time I was able to reproduce the problem accurately with your new instructions.
Got it. Obviously I didn't put two and two together and understand that the new operation was meant to correct that problem. Thank you for taking the time to explain it to me.
The new change record looks good to me.
With my better understanding of the issue I re-reviewed the code. It still looks good to me overall. I only found one documentation issue to nitpick. I hope you don't mind me setting this to Needs Work for that.
Finally, I noticed #849602: Update 'username' theme template to use 'view label' operation. in the Needs Review queue earlier today. I checked its MR and it is modifying the same hook. These two will conflict with each other.
Comment #59
dcam commentedOk, we cross-posted. My suggestion was already applied. So I'm going to go ahead and RTBC this.
Comment #60
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #61
godotislateRebased and set this issue version to main.
Comment #64
catchCommitted/pushed to main and 11.x, thanks!
Comment #68
berdirNote: This could have some tricky side effects in custom/contrib code if they did something special on the previously used operation. We noticed #3588357: Handle new "view linked label" access operation in our performance tests, which is minor, but there might be cases where extra access checks either allowed or disallowed access, which now no longer works. It's likely just a bug and not a security issue for them, as it just controls whether or not there is a link, but still a bit concerning.
Maybe the CR can more clearly mention that access hooks might need to handle this operation.