Problem/Motivation
When using web services to access group content, for example, GET /jsonapi/group/{group bundle}. JSONAPI or dynamic caching would cache the result, even if the authentication has changed.
Steps to reproduce:
Install sandbox on simplytest.me with
1. durpal 8.4.0-beta1
2. jsonapi 8.x-1.x-dev or Core REST module
3. group 8.x-1.x-dev
Do somthing:
1. Enable Group ,JSON API module , HTTP Basic Authentication.
'4 modules have been enabled: HTTP Basic Authentication, Group, JSON API, Serialization.'
2. Add group type :DrupalCamp // `/admin/group/types/add`
3. Request with admin user. Write down the information. // GET `/jsonapi/group/drupalcamp`
4. Request with Anonymous. Write down the information: // GET `/jsonapi/group/drupalcamp`
Expected result: 403 access denied. but, the actual result is the same as Step 3.
jsonapi/group_content/* has the same problem.
Proposed resolution
Add cache context: user for group and group content.
Before Extend the entity access system with a new grants API (and deprecate the query-alter-based node grants API)
or
Entity Access Policies
be finished, because it can't be finished soon.
| Comment | File | Size | Author |
|---|---|---|---|
| #54 | interdiff-51-54.txt | 7.64 KB | kristiaanvandeneynde |
| #54 | group-2906082 -54.patch | 86.52 KB | kristiaanvandeneynde |
Comments
Comment #2
skyredwangCleaned up the description a little bit.
I also ran into caching problems with JSONAPI before. But, completely different set up to the case above.
Comment #3
e0ipsoThanks for the report @skyredwang. However the
jsonapimodule is not doing anything special about authentication. It only uses the authentication providers.Can you test that same scenario but using REST core instead of JSON API? That may be able to confirm that the problem is outside of JSON API.
Comment #4
e0ipsoComment #5
dravenkI have been reproduce the bug in REST core.
Steps to reproduce:
Install sandbox on simplytest.me with
1. durpal 8.4.0-beta1
2. group 8.x-1.x-dev
3. restui 8.x-1.15
Do somthing:
1. Enable Group ,HAL, HTTP Basic Authentication,REST UI,RESTful Web Services,Serialization.
2. Enable `/group/{group}: GET,`
GET
formats: hal_json, json
authentication: basic_auth .
3. Add group .
4. Request with Admin user. Write down the information. // GET `/group/1?_format=hal_json`
5. Request with Anonymous. Write down the information. // GET `/group/1?_format=hal_json` .
Expected result: 403 access denied. but, the actual result is the same as Step 4.
Comment #6
skyredwangComment #7
skyredwangComment #8
wim leersUnpublishing.
Comment #9
mlhess commentedThis was unpublished due to the security vulnerability, but given that there is no stable release, this can be handled in public.
Comment #10
dravenk@Wim Leers
I was tested another patch.That will be fixed this issues. #2896603: Static caching issue when loading group roles by user & group
Comment #11
dravenkComment #12
wim leersComment #13
wim leersHow can this be RTBC if there is no patch?
Comment #14
dravenk@Wim Leers
I'm sorry. I just knew it now
Comment #15
kristiaanvandeneyndeI just noticed this issue, but I hardly think the caching that is being fixed in the other issue has any effect on this one.
That said, the access control handler for Group does not allow anonymous access to group/1 unless you have the permission to view that group. Correct me if I'm wrong, but doesn't the REST layer respect the entity types' access control handlers? If so, you should not get different results when switching between a browser request or a REST request.
Comment #16
dravenk@kristiaanvandeneynde
I test with simplytest.me . I found after uninstall Internal Dynamic Page Cache module that output is right. Caching always in this API
/jsonapi/group/{group bundle}otherwise.Comment #17
dravenkI try to read the @Wim Leers 's blog which about Dynamic Page Cache. Thanks for the heavy work on deal with cache.
If any suggestions?
Comment #18
kristiaanvandeneyndeThis is a just an educated guess, but maybe you could try adding the group_membership.roles.permissions cache context to every page? It's a change I've been contemplating for doing in Group as I fear we'll have to do that either way.
Comment #19
lawxen commentedThe X-Drupal-Dynamic-Cache:HIT indicates that the cache come from Dynamic_page_cache module.
when visit /jsonapi/group/business
The cache_dynaic_pagic_cache table will generate two records:
The two cids are:
We can see the two cids don't have user or user role or group role or user permission information. So every user visit /jsonapi/group/business will get the same cache result.
Cache cid was generated by cache key and cache context.
The root problem is group's cache context.
proposed resolution:
Add "group" and "group_membership.roles" or group_membership.roles.permissions cache context to group's cache like #18 said.
Comment #20
lawxen commentedI set http.response.debug_cacheability_headers: true in development.services.yml.
Then we can see the X-Drupal-cache-Contexts:
url.query_args:fields url.query_args:filter url.query_args:include url.query_args:page url.query_args:sort url.site user.permissionsIt already has user.permissions
Then I'll try whether group_membership.roles.permissions cache context which @kristiaanvandeneynde suggest on #18 can solve this problem.
Comment #21
e0ipso@caseylau thanks for looping back into this. Let us know if that fixes your problem. Also, please link any issues in the Group module here as well.
Comment #22
lawxen commenteddeleteComment #23
lawxen commentedComment #24
lawxen commented@e0ipso @kristiaanvandeneynde Add "group_membership.roles.permissions" to group entity can't solve this issue.
I add
directly to Group.php to make a test, and ensure that the context was added successfull, But the cache issue still there.
Comment #25
lawxen commentedAfter adding group_membership.roles.permissions
GroupMembershipPermissionsCacheContext->getContext
$this->hasExistingGroup() get null cause it don't take effect.
\Drupal\group\Cache\Context\GroupMembershipCacheContextBase->hasExistingGroup()
\Drupal\group\Context\GroupRouteContextTrait->getGroupFromRoute()
We can see group information come from route object.
But route jsonapi/group/business don't come from group module.
So getGroupFromRoute() return Null
Maybe we can change the method of getting group information from route.
Comment #26
kristiaanvandeneyndeOk so now I see what's going on here. What you're dealing with is yet another example of why we need something like the node grant system for all entity types. The cache context I suggested you use only works when there is a single group in the route; e.g.: group/1, group/2/edit, ...
Because you're dealing with a list of groups and there is no way for Drupal to figure out which ones any given user has access to from a cache context perspective, you'll end up with stale caches when it comes to listing groups. Until #777578: Add an entity query access API and deprecate hook_query_ENTITY_TYPE_access_alter() lands, there is no easy solution but to turn off caching for listings of groups by setting max-age to -1.
Edit: Or add the "user" cache context.
Comment #27
lawxen commented@kristiaanvandeneynde
Thanks for the advice.
I have tested it, works:).
Besides jsonapi/group_content/* has the same issue.
Comment #28
lawxen commentedDo this
Comment #29
kristiaanvandeneyndeThe patch you suggest won't do because it will add the user cache context all over the place, even in places where we absolutely don't need it. This is no longer a bug report, but a feature request. It could do with an issue summary update to reflect that.
What we need is a cache context that varies by group permissions, which is hard to pull off for all groups on a site at once. The new entity access policies system might help with that, but to be fair I think we should advise all lists that depend on group permissions to use the 'user' cache context for now.
Comment #30
lawxen commentedI create a issue about jsonapi/user/user, Not same problem,
But has some similarity, Both have cache issue.
Comment #31
lawxen commentedComment #32
lawxen commented#28's patch has two unneeded line code
The issue summary needs to be updated.
Comment #33
lawxen commentedComment #34
lawxen commentedHave a small comment error.
I'm very sorry for uploading so many patches and comments to influence you because of my negligence.
Comment #35
kristiaanvandeneyndeI'm sorry but I can't accept a patch that adds the user cache context to lists. Some lists might not care about who can see what because they know everyone with access to the list has access to all entries. Not serving all of those users the same cache entry then is a step backwards in terms of performance.
The real issue is that core does not hand us any tools to cache entity lists based on access other than permissions or roles. So the best fix would be to figure out a way to create a cache context that compares someone's group permissions in a way that it's relevant to the page content. Given the complexity of that, it might be better to figure out another way of doing it.
Maybe for every entry in the table, add a cache context that says entity_access:entitytype.id.operation where you substitute the last three parts for the actual entity type, id and operation. Although that would increase the potential amount of cache entries by a lot, even when the set of group permissions would only imply a handful of possibilities.
Comment #36
wim leersThis seems the same as #2952714: Group module's GroupAccessResult::allowedIfHasGroupPermission(s)() does not include cacheability? If not the same, then definitely closely related.
Comment #37
lawxen commented2906082-34.patch can't solve this issue completely,
Like https://www.drupal.org/project/drupal/issues/2982770
Two user, If one user request(use right password) the jsonapi/group_content/...... get empty list.
The other user will get the empty list too.
This comment's patch both solve this issue and Group module's GroupAccessResult::allowedIfHasGroupPermission(s)() does not include cacheability
But whether this patch is just a temporary or the most appropriate solution, this needs more exploration
Comment #38
kristiaanvandeneyndeFor those interested, I have a fix in a separate branch that I hope to merge into the main one soon. It still needs tests and some polish first, though.
Comment #39
kristiaanvandeneyndeOkay so after weeks of working on this, I finally have a patch with tests to try!
This introduces three new cache contexts, one of which is now actively being used in GroupAccessResult:
Comment #41
kristiaanvandeneyndeFixed a minor mistake and fixed failing tests in other issue.
Comment #43
kristiaanvandeneyndeMight have to reconsider user.is_group_member:%group_id to become user.group_memberships as it's too dynamic on websites with many groups. Currently asking around on Slack about that.
Also noticed the user.group_permissions cache context could do with a unit test. Let's see what testbot thinks about this first, though.
Comment #44
kristiaanvandeneyndeGuess we can keep user.is_group_member:%group_id, but have to document it should not be used for very dynamic elements (such as the group operations block). It could still prove useful for, let's say, a call-to-action urging people to join a particular group.
This means I need to adjust the group operations block to cache per user, user.group_memberships (not a fan of that) or a combination of user.is_anonymous and user or max-age 0, given how we should be able to cache that block just fine for all anonymous users.
Comment #45
kristiaanvandeneyndeThis adds the missing unit test and fixes a few minor flaws. Still needs a final pass, especially regarding the cacheable metadata of the GroupOperationsBlock.
Comment #46
lawxen commentedAfter use this patch, when we use member account to visit /jsonapi/group_content/....., we got error:
Comment #47
skyredwangComment #48
kristiaanvandeneynde#46 I got a fix coming for that one!
Comment #49
kristiaanvandeneyndeComment #51
kristiaanvandeneyndeComment #52
lawxen commentedtest for group-2906082 -51.patch
All passed 🎉
Comment #53
kristiaanvandeneyndeExcellent news! 🎉🎉🎉
Comment #54
kristiaanvandeneyndeMoved the cacheable metadata for group operations to a dedicated method. This is the only way to reliably set cache contexts when there are no links because of a context value. Let's see what the tests have to say, especially regarding coding standards and then we can commit if it stays green.
Comment #57
kristiaanvandeneyndeSpecial thanks to casylau and Wim Leers for helping out on this issue and the related one(s)! Also adding berdir because he gave me meaningful insights into the caching system over Slack, which allowed this patch to be written.