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.

Comments

dravenk created an issue. See original summary.

skyredwang’s picture

Title: JSON API use auth information by first caches » When use JSONAPI with Group, caching stops working properly
Component: Miscellaneous » Code
Issue summary: View changes
Priority: Normal » Major

Cleaned up the description a little bit.

I also ran into caching problems with JSONAPI before. But, completely different set up to the case above.

e0ipso’s picture

Thanks for the report @skyredwang. However the jsonapi module 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.

e0ipso’s picture

Status: Active » Postponed (maintainer needs more info)
dravenk’s picture

Project: JSON:API » Group
Status: Postponed (maintainer needs more info) » Active

I 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.

skyredwang’s picture

Issue summary: View changes
skyredwang’s picture

Title: When use JSONAPI with Group, caching stops working properly » When using web services with Group, caching stops working properly
wim leers’s picture

Unpublishing.

mlhess’s picture

This was unpublished due to the security vulnerability, but given that there is no stable release, this can be handled in public.

dravenk’s picture

@Wim Leers
I was tested another patch.That will be fixed this issues. #2896603: Static caching issue when loading group roles by user & group

dravenk’s picture

Status: Active » Reviewed & tested by the community
wim leers’s picture

wim leers’s picture

Status: Reviewed & tested by the community » Active

How can this be RTBC if there is no patch?

dravenk’s picture

@Wim Leers
I'm sorry. I just knew it now

kristiaanvandeneynde’s picture

I 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.

dravenk’s picture

@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.

dravenk’s picture

I 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?

kristiaanvandeneynde’s picture

This 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.

lawxen’s picture

Issue summary: View changes
StatusFileSize
new45.59 KB
new165.32 KB


The 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:

  1. response:[request_format]=html:[route]=jsonapi.group--business.collection35786c7117b4e38d0f169239752ce71158266ae2f6e4aa230fbbb87bd699c0e3
  2. response:[request_format]=html:[route]=jsonapi.group--business.collection35786c7117b4e38d0f169239752ce71158266ae2f6e4aa230fbbb87bd699c0e3:[url.query_args:fields]=:[url.query_args:filter]=:[url.query_args:include]m-PWCW9TcAb0aNazDIBPbvqdWfBpkO_U1gfeV6d9Gxk

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.

lawxen’s picture

Issue summary: View changes
StatusFileSize
new219.95 KB

I 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.permissions

It already has user.permissions

Then I'll try whether group_membership.roles.permissions cache context which @kristiaanvandeneynde suggest on #18 can solve this problem.

e0ipso’s picture

@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.

lawxen’s picture

StatusFileSize
new226.84 KB

@e0ipso @kristiaanvandeneynde Add "group_membership.roles.permissions" to group entity can't solve this issue.

I add

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    $this->cacheContexts[] = 'group_membership.roles.permissions';
    return $this->cacheContexts;
  }

directly to Group.php to make a test, and ensure that the context was added successfull, But the cache issue still there.

lawxen’s picture

After adding group_membership.roles.permissions

GroupMembershipPermissionsCacheContext->getContext

  public function getContext() {
    // If there was no existing group on the route, there can be no membership.
    if (!$this->hasExistingGroup()) {
      return 'none';
    }

$this->hasExistingGroup() get null cause it don't take effect.

\Drupal\group\Cache\Context\GroupMembershipCacheContextBase->hasExistingGroup()

  public function __construct(RouteMatchInterface $current_route_match, AccountInterface $user, EntityTypeManagerInterface $entity_type_manager) {
    $this->currentRouteMatch = $current_route_match;
    $this->group = $this->getGroupFromRoute();
    $this->user = $user;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * Checks whether this context got an existing group from the route.
   *
   * @return bool
   *   Whether we've got an existing group.
   */
  protected function hasExistingGroup() {
    return !empty($this->group) && $this->group->id();
  }

\Drupal\group\Context\GroupRouteContextTrait->getGroupFromRoute()

  public function getGroupFromRoute() {
    $route_match = $this->getCurrentRouteMatch();
    
    // See if the route has a group parameter and try to retrieve it.
    if (($group = $route_match->getParameter('group')) && $group instanceof GroupInterface) {
      return $group;
    }
    // Create a new group to use as context if on the group add form.
    elseif ($route_match->getRouteName() == 'entity.group.add_form') {
      $group_type = $route_match->getParameter('group_type');
      return Group::create(['type' => $group_type->id()]);
    }

    return NULL;
  }

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.

kristiaanvandeneynde’s picture

Ok 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.

lawxen’s picture

@kristiaanvandeneynde
Thanks for the advice.

add the "user" cache context

I have tested it, works:).

Besides jsonapi/group_content/* has the same issue.

lawxen’s picture

add the "user" cache context

Do this

kristiaanvandeneynde’s picture

Title: When using web services with Group, caching stops working properly » Figure out a way to cache lists using group permissions
Category: Bug report » Feature request
Priority: Major » Normal
Issue tags: -cache, -authentication +Needs issue summary update

The 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.

lawxen’s picture

I create a issue about jsonapi/user/user, Not same problem,
But has some similarity, Both have cache issue.

lawxen’s picture

StatusFileSize
new380 bytes

lawxen’s picture

Status: Needs review » Postponed
StatusFileSize
new1.03 KB
new380 bytes

#28's patch has two unneeded line code

The issue summary needs to be updated.

lawxen’s picture

Title: Figure out a way to cache lists using group permissions » Add some cache context for group and group content
Priority: Normal » Major
Issue summary: View changes
Status: Postponed » Needs review
StatusFileSize
new1.21 KB
new1.07 KB
  1. Change summary to be a feature request.
  2. Change the priority to major, because jsonapi or resetapi can't be used correctly on group or group content now.
  3. And add enough information on comment in this patch.
lawxen’s picture

StatusFileSize
new1.21 KB
new902 bytes

Have a small comment error.
I'm very sorry for uploading so many patches and comments to influence you because of my negligence.

kristiaanvandeneynde’s picture

Title: Add some cache context for group and group content » Figure out a way to cache lists using group permissions
Status: Needs review » Needs work

I'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.

wim leers’s picture

lawxen’s picture

2906082-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

kristiaanvandeneynde’s picture

For 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.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
StatusFileSize
new67.8 KB

Okay 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:

  • user.group_permissions (used in GroupAccessResult)
  • user.is_group_member:%group_id
  • route.group

Status: Needs review » Needs work

The last submitted patch, 39: group-2906082-39.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
StatusFileSize
new67.8 KB

Fixed a minor mistake and fixed failing tests in other issue.

Status: Needs review » Needs work

The last submitted patch, 41: group-2906082-41.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Might 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.

kristiaanvandeneynde’s picture

Guess 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.

kristiaanvandeneynde’s picture

StatusFileSize
new76.92 KB
new14.08 KB

This adds the missing unit test and fixes a few minor flaws. Still needs a final pass, especially regarding the cacheable metadata of the GroupOperationsBlock.

lawxen’s picture

After use this patch, when we use member account to visit /jsonapi/group_content/....., we got error:

The website encountered an unexpected error. Please try again later.
Error: Call to undefined method Drupal\Core\Session\AccountProxy::getCacheTagsToInvalidate() in Drupal\group\Access\GroupPermissionsHashGenerator->buildMemberPermissions() (line 297 of modules/contrib/group/src/Access/GroupPermissionsHashGenerator.php).
skyredwang’s picture

Status: Needs review » Needs work
kristiaanvandeneynde’s picture

#46 I got a fix coming for that one!

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
StatusFileSize
new2.05 KB
new77.06 KB

Status: Needs review » Needs work

The last submitted patch, 49: group-2906082-49.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
StatusFileSize
new77.25 KB
lawxen’s picture

Status: Needs review » Reviewed & tested by the community
kristiaanvandeneynde’s picture

Excellent news! 🎉🎉🎉

kristiaanvandeneynde’s picture

StatusFileSize
new86.52 KB
new7.64 KB

Moved 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.

  • 20820d6 committed on 8.x-1.x
    Issue #2906082 by caseylau, kristiaanvandeneynde, Wim Leers: Figure out...

kristiaanvandeneynde’s picture

Status: Reviewed & tested by the community » Fixed

Special 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.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.