Maybe I'm missing something, but the cache doesn't seem to have a relevant cache tag and I also can't see an API on ChainGroupPermissionCalculator for that.
To do that through cache tags, you need something like a user.group_memberships:ID cache tag, that you can invalidate whenever that specific user is added to a group. Basically just like the list cache tags of entities. There is the group_content cache tag and the user cache tag but nothing to listen on new group content entities.
I'd argue that the user cache tag isn't even really needed, because changing something on the user shouldn't affect is permissions, not with the default calculators?
And not sure about the group_content, if you have the list cache tag, then that might be more efficient? If a user is member of 20 groups, then you have 20 cache tags that all need to be loaded and verified. While the list cache tag could be triggered if any of these entities is created, updated or deleted.
| Comment | File | Size | Author |
|---|---|---|---|
| #22 | drupal-3079463-22.patch | 15.22 KB | kristiaanvandeneynde |
| #22 | interdiff-21-22.txt | 3.97 KB | kristiaanvandeneynde |
| #8 | group-user-group-add-invalidate-3079473-8.patch | 5.02 KB | berdir |
| #2 | group-user-group-add-invalidate-3079473-2.patch | 1.61 KB | berdir |
Comments
Comment #2
berdirSomething like this, didn't do any of the suggested cleanup yet and it would also be neat if there would be a postSave() method on the plugin, but there's already similar logic in GroupContent::postSave().
I'm not sure how tests are passing without this? No example of having a user, checking access and then adding that user to a group?
Comment #4
kristiaanvandeneyndeOkay so we have this code in
I recall seeing an issue by your hand that complained about this very re-saving of content (in this case user). So maybe you have a patch that prevents this, in which case the user cache tag added to a set of member permissions does not get cleared?
Comment #5
kristiaanvandeneyndeOh and as you can see by the @todo, I was meaning to introduce a group_memberships:UID cache tag of some sorts that gets cleared whenever you join or leave a group.
If we generalize that to
group_content:<bundle>:<target_entity_id>, we would be one step closer to getting rid of that re-saving you disliked. Another important one being the ditching of node grants for entity access queries.Comment #6
berdirWe discussed this a while ago in slack, quick summary of our discussion and also replies to your questions.
* Actually, I was one of the people who argued in favor of the resave. It's problematic, but also required at the moment, for url alias generation for example. Besides, this is the saving of the group_content entity, resaving the target entity has nothing to do with that code path there.
* So we're not using any specific patches that would result in this only happening then IMHO. But we'll verify and see if we can create a failing test.
* As we discussed, the user cache context is unnecessary complexity here, we already have the user id, we don't need to go through the indirect cache context. It's also wrong as it is always the current user, while the API accepts an account argument. We should be able to verify this as well in a test, by requesting the permissions for different users.
*
$calculated_permissions->addCacheableDependency($user)should also not be necessary, as we don't care about saving the user entity, our cache is not affected by that in the default implementation at least. If someone has permissions depending on e.g. a field on the user, then they should add this.*
$calculated_permissions->addCacheableDependency($group_membership), not quite sure about this one, but I think we can skip it too, saving a group membership will also always invalidate our own cache tag, we don't need to also check for the group_content entity cache tag.To summarize, the test should create multiple users groups and group roles. First check the permissions when the users are not yet in the group, then assign these users to different groups with different roles, then make sure the returned permissions are correct, then update some memberships, also delete, and in all cases, make sure that the permissions are correct.
Comment #7
kristiaanvandeneyndeWell I always welcome more tests, so that's a plus :)
Same goes for any entity access control handler and other core API's, though. That's what this change record (and issue) was trying to fix, I believe: https://www.drupal.org/node/2633002. So while I recognize the problem, I'm wondering if this particular problem isn't bigger than just Group.
$calculated_permissions->addCacheableDependency($user)is only being done because I haven't had the time yet to properly implement a cache tag which is cleared when your membership is updated. But because we know a user is also re-saved when a membership is updated, we add the user dependency as a poor man's membership cache tag. Both the one you suggest in the IS and the one I mention in various @todo around the codebase show that we could really use such a cache tag.$calculated_permissions->addCacheableDependency($group_membership)is necessary because your group roles are stored on the membership and so a membership save might mean different roles.Comment #8
berdirYes, you're right, the $user cache dependency should take care of invalidating it when a new membership is created due to resaving the user entity. And the membership cache tag should
I'm not sure why this doesn't work for me but it reliable doesn't. This is a somewhat special setup though, I'm testing this with behat and I'm creating users and then memberships in a (very) long-running behat process and then the caches aren't invalidated. Maybe something about cache invalidation isn't working due to the long-running processes and it all happening at once?
That said, with my new cache tag that is only invalidated if we either have a new membership or a role change, it is safe to drop both IMHO. I'm not sure there is a benefit of having a more generic
group_content:<bundle>:<target_entity_id>, we'd lose the optimization of whether this is a relevant change and most other cache invalidations are probably more about specific group assignments, aka the group_content entity.I'll try again to figure out what exactly is going on in my project and I'll make sure that this patch also works for us. If I can't figure it out then IMHO this still makes sense as a task ;)
Comment #9
berdirRerolled, a lot has changed apparently :)
I don't get that persistent cache context stuff yet, so for now, just removed the user cache context there again.
Comment #10
mbovan commentedThis patch tries to address the last paragraph in #6 and adds suggested test scenarios.
I was getting an error in tests with
::calculateMemberPermissions()in a scenario when a user is removed from a group. I am not sure I fully understand::calculateMemberPermissions()and whether this method should be called on a user entity that is not part of the group or not... However, addingCache::invalidateTags(['user:group_memberships:' . $group_content->getEntity()->id()]);seems to fix the issue.Comment #11
mbovan commentedThe interdiff in #10 is not fully correct.
Comment #12
berdirI still don't *completely* understand the problems we had, but I can confirm that the new test coverage doesn't just fail with the previous patch but also against HEAD, due to the current user cache context. And this does solve our problems. Also, more importantly, it is more correct, simpler and efficient (fewer cache tags to verify, only invalidated when necessary)
As I said in #9, the purpose of that persistent cache context stuff is unclear to me and might be unnecessary now, but IMHO this is ready.
Comment #13
kristiaanvandeneyndeOkay, I want to fix this issue before a full release (so soon™). But this obviously needs to see proper cache tags go in first, so let's create an issue for that. See: #3090833: Introduce more list cache tags for GroupContent
Comment #14
kristiaanvandeneyndeI'm in the process of refactoring your patch at DrupalCon. I'd like to explain the intricacies of the new cache context stuff, but can't do that without explaining how the variation cache works. And I guess you don't want to get dragged down that rabbit hole just yet :D
I'll make my changes and run your test locally to see why it's breaking with the user cache context. I'm assuming because the test doesn't set the current user, whereas the user cache context uses that. But as I mentioned before, that is a problem we still have in core which I would prefer to solve there and then mimic here.
See #2667018: Do not use user.permissions cache context in AccessResult? and #2628870: Access result caching per user(.permissions) does not check for correct user
Comment #15
berdirI know core has those problems too, but there really is no reason to use the cache context here. That's useful when you don't have control over the cache keys, e.g. add some stuff in a block or to some existing thing.
Using cache context for things that you know in advance just adds unnecessary complexity. And a generic solution is so much harder to solve.
This is quite the same as e.g. the default cache *keys* for rendering entity (\Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults()), that also includes the node ID. Yes there is no cache context for that, but it wouldn't make sense to use it even if there were.
Also, in regards to cache tags, as I said in #8, this specific cache tags has advantages over a more generic one because it has fewer invalidations, saving a membership without a role change doesn't invalidate it.
Comment #16
kristiaanvandeneyndeAs discussed on Slack, we're going to leave the whole user cache context thingy out of scope for now :)
True, I was in favor of removing said logic because we might end up special-casing a lot of cache tags then (slippery slope fallacy). I'll focus on adding the other cache tags this week and then see whether we can keep the one introduced here. We might want to make an axception here, but no promises!
Comment #17
berdirSo, I was able to track down what is going on in my project. This is the inline comment that I added to our behat step:
That said, I still believe that my/our proposal makes sense, it fixes problems that are hard/impossible to solve in general and is more efficient too. I understand what you said in regards to being as flexible as possible, but IMHO, what needs to be flexible is the public API and that is basically \Drupal\group\Access\ChainGroupPermissionCalculatorInterface::calculatePermissions() (and the member version). That there even is caching is pretty much an implementation detail, and if you have a <1% use then IMHO, it's acceptable having to override a bit more.
That said, because we no longer have a need for a quick fix, it IMHO doesn't make sense to split this issue up just for the cache tags.
Comment #18
kristiaanvandeneyndeCool, thanks for the feedback.
Your comment block shows that something is trying to invalidate user:ID twice where there is nothing being written to a cache with the user:ID tag in between both cache clears. So it only makes sense that the tag is not invalidated the second time.
It still leaves me wondering how this affects the permissions cache, though as that currently does write to the cache using the user cache tag. I assume it is because you are trying to clear the cache tag of someone other than the active user, which again leads to our whole discussion we had on Slack :)
I'm still somewhat opposed to simplifying this layer because I believe hard-coding cache ID parts that actually represent a cache context leads to inflexible code. I go into detail here https://youtu.be/rAecKDWOPmY?t=909 (4 minutes onwards from the provided timestamp)
In the meantime I've fixed #3090833: Introduce more list cache tags for GroupContent, so I can now have a look at the special members cache tag with the generic ones in place already.
Comment #19
kristiaanvandeneyndeFWIW, I just gave it some more thought and you're right about needing to change the cache ID. Because as far as the permission calculator goes, the question is not "What are the permissions for the current user", but rather what are they for any given user.
Unlike the RouteProvider example in my session, it is valid to ask for a another user's member permissions during the lifespan of a request because A) the Group API supports that and B) I can think of valid use cases where you want to check a number of users' group permissions (e.g.: Send a mail to anyone who can see this group).
That said, it is out of scope for this issue, but should definitely be changed somehow. Might be tricky because some of the code does check for the current user's group permissions and I'd still like that part to vary by cache contexts rather than manually constructed dynamic cache IDs.
Maybe I need to move the cache around a bit, we'll see.
Comment #20
kristiaanvandeneyndeAttached is a patch with the new cache tags being used. The new test will definitely still go red as per #19
Comment #21
kristiaanvandeneyndeThis adds new logic to the chain calculator to support accounts other than the current user. It's still out of scope for this issue, but let's upload it here as a proof of concept. Will separate said logic out into its own ticket before committing.
Comment #22
kristiaanvandeneyndeSmall interdiff before I split this up into #3099331: Make sure the permission calculator works for non-current-user accounts
Comment #23
kristiaanvandeneynde#3099331: Make sure the permission calculator works for non-current-user accounts was fixed just now. Please confirm if the issue still persists in dev @Berdir.
I'd like to mark this issue as fixed with full credit to those who contributed, even if the patch moved to another issue.
Comment #25
kristiaanvandeneyndeComment #26
kristiaanvandeneyndePlease file a new ticket if this persists.