Based on #2894670-6: Access check breaks result count and paging: Add a processor which, for every indexed item (or maybe just items from selected datasources?), adds a field with all roles that have access to that item. At search time, of course, it would then place a filter on all queries to only return items to which the user (based on their roles) has access.
It would not work in 100% of cases, of course, and we'd have to clearly state that, but it would probably still be a significant improvement for sites that do have some access mechanism on entities other than nodes.
(Speaking of nodes: Should probably not work on nodes (and comments?), since we already have better systems in place for those.)
| Comment | File | Size | Author |
|---|---|---|---|
| #30 | 2898334-30--role_based_access_check.patch | 16.64 KB | drunken monkey |
| #12 | 2898334-12--role_based_access_checks.patch | 4.34 KB | andreasderijcke |
Comments
Comment #2
drunken monkeyComment #3
andreasderijckeOne of our projects required this for Commerce Product entities, which I've generalized for any entities, excluding nodes and comments as suggested.
This processor is based on the Content Access processor.
With the lack of entity alternative for node_access_grants() (did I overlook it?), similar function is included.
In time, https://www.drupal.org/project/drupal/issues/777578 should provide something for this.
Proposal attached.
Comment #4
drunken monkeyThanks a lot for working on this!
Your patch looks like a good start, but it doesn't use quite the approach I had in mind. I would do this:
$item->getDatasource()->checkItemAccess($item->getOriginalObject(), $account);to see whether the role has access to the item. Save all roles with access to the item to the field we added.$query->addCondition($access_field, $account->getRoles(), 'IN').Since, as you've pointed out, entities in general don't have a grant/access system, this is really the best we can do at the moment, I think. We should just be very clear that this is not guaranteed to work in the processor's description – see the attached patch revision.
Probably we don't need to exclude node and comment datasources, we should just recommend the "Content access" processor for those – and add a config form for selecting the datasources this new processor should apply to.
We also don't really need to restrict ourselves to entities,
Datasource::checkItemAccess()will work just as well on any other kind of search item.Anyways, does this make sense to you?
Comment #5
andreasderijckeIt does make sense.
Wasn't aware of the checkItemAccess() function, was entirely extrapolation from ContentAccess.
Not sure yet wat the implications are for entities and permission to view all of its bundles or just specific ones. Will see when we get there.
Working on a rewrite including:
Hope to post new version tomorrow.
Comment #6
andreasderijckeUpdated version using suggested approach. However, 2 issues:
1. Manipulating roles and faking 'authenticated' user on a new User object seems problematic, as many underlying functionality requires a full/existing user object. So I ended up using user 1 (the only user we can be sure exists), and manipulating its roles. This seems work more or less as desired, keeping in mind user '1's special status.
However, during testing I got stuck on the next issue:
2. Caching
Using checkItemAccess() will end up calling Drupal\Core\Entity\EntityAccessControlHandler::access, which uses static cache for given user and entity. Hence, check on first role for authenticated users will return the expected result, all following checks will return the cached result.
Any suggestions?
Comment #7
drunken monkeyUsing an existing user, and especially user 1, seems a bit dangerous regarding side effects, or possible permanent changes. But I haven't tried this myself, so if you say it doesn't really work otherwise, I'll have to believe you. However, it seems using different (new) user objects would/might also resolve the caching problem? So my intuition would still be to get it working with separate "mock" users.
Otherwise, if this would generally be working fine and only the caching being a problem: all caches in Drupal should be clear-able, as a rule, so you'd just have to find the cache in question and clear it before/after each check. (Definitely once in the end, too, to avoid bleeding through to other code.)
Ohter remarks:
Definitely at least clone it each time, to make sure none of this is bleeding through to other code.
This part here is definitely wrong, since you can't be sure what the field ID will be. Instead, you have to look for the field using $this->findField().
Comment #8
borisson_I discussed this with @andreasderijcke at the DUGBE sprint on saturday. This cache is a static cache, not something that is actually saved in a database. It's a protected property on a class.
I mentioned that we might be able to use reflection to reset this cache, but got angry looks from Nick and swentel, so we didn't investigate that further.
We also shortly discussed creating a user, but creating a user every time we're indexing seems like asking for performance troubles (we might have to create a new user every 5 minutes, and deleting it afterwards).
It might be an option to create new locked users that we can reference from code, those would only have to be created once, but they could be used for logging in as well, and that doesn't sound like the best plan either (I think).
Comment #9
andreasderijckeWell yes! However creating a mock registered user is the/my problem. You cannot add 'authenticated' role to a new User object. User entity code prevents this. To me, this implies replicating user registration in code, which i'd rather avoid.
In addition, even if we manage to mock a User object with role 'authenticated', without having an user id, function getRoles() (and possibly others) will still consider this user as 'anonymous'.
Therefore, using a real user as starting point, seems a nicer way to go.
Although, i'm not happy using user '1', creating a dedicated user for indexing also adds additional complications, as mentioned by @borisson_. Also, from website owner perspective, it feels wrong.
I'll check if indexing works using a blocked user. If so, the static cache (also in User entity class) remains a blocker.
Comment #10
drunken monkeyAfter this Core change, we might have a chance to update the code to use such a memory cache, which we'll then be able to clear (I'd think).
Otherwise, I'm not sure why we shouldn't use reflection? Sure, it's overkill and the code will look pretty ugly – but it should work, and I can't really see any actual problems it might cause.
On the other hand, this feature isn't that important, I'd say, so going to so much trouble might not be worth it. But if andreasderijcke wants to continue working on it, I don't think I'd veto a reflection approach.
Or did Nick and swentel have any actual concerns, apart from the instant “Ew, no!” reflex (which I got myself, I have to admit).
Comment #11
borisson_No, I don't think there were any actual concerns apart from that reflex :). I agree that would be the most simple way to solve this issue. It might not be the most pretty code, but if it works and we add sufficient tests I don't think it'll be a big problem.
Comment #12
andreasderijckeAfter more studying of the user module, I realized you can create a fake user (object) with id of your choosing.
I used the sequences value increased with an arbitrary value, to avoid collision with new users created during indexing.
Result looks good in the database, still have to test this on an real use case.
Comment #13
drunken monkeySounds good, thanks! Looking forward to your real-world test results.
We'll definitely need some automated tests for this, though, before we can commit it.
Comment #14
miroslav-leeDoes not works patch.
I get the user ID with an error.
Which means I can't load the user object
Comment #15
miroslav-leeI created a processor that checks each entity for access.
This works for my project.
Comment #16
miroslav-leeComment #17
drunken monkey@ miroslav-lee: Thanks for reporting, but your patch in #15 does something completely different than the one proposed here, so it’s not really relevant to this issue.
Comment #18
sam152 commentedThis would be a great processor to add, especially given most sites I've encountered have opted not to use the grants system for access. I believe this would work better for the majority of use-cases, even for nodes and comments.
@miroslav-lee, yeah, any access check that depends on reloading a real user will fail. Under that circumstance, the access check is not role based, so this kind of access plugin is not appropriate. Additionally, I don't believe the access system stipulates a user must be saved, so for cases like the module you've highlighted, bug reports can be filed and fixed to exit gracefully when access checking an unsaved user.
Comment #19
sam152 commentedHere is a tweaked approach based on the work already completed by @andreasderijcke. I renamed the plugin class but git did not recognise this as a move since there were so many changes, instead a delete + add, so the interdiff was not really useful.
Here is a summary of the changes I've implemented and why:
ProcessorPluginBase, since it does not conceptually build upon the access checks inContentAccess.ContentAccess, which weren't provably required by the test coverage. Those may need to come back, but I'd like to understand the scenario for each case better + add a test.Would appreciate any reviews or feedback.
Comment #20
drunken monkeyThanks a lot for your work, great job!
I still had a lot of little issues with the patch (see the attached revision), but the overall direction is great now and it seems to work perfectly fine, too.
Please review my revisions and see whether they look good to you and whether this still works for you. (Test/reviews by others would also be really helpful in getting this committed!)
In any case, thanks again!
Comment #21
sam152 commentedAll those changes look great @drunken monkey. This gets a +1 from me.
I'm not sure this comment adds much value, since an instance of UserSession is not an entity and can't be saved.
Double space in the comment.
Comment #22
drcolossos commentedThis works nicely but I would like to clarify something:
I spend some time debugging because at first it did not work as expected: we have granted the "access_content" (View published content) to Anonymous and Authenticated roles. It seems as if this does not inherit properly to the other roles.
If I have a role "foo", I expected it to have "access_content" as well because I checked "Authenticated" (and this removes the checkboxes for all the other roles). But this is not the case. When RoleAccess.php iterates the roles, only Anonymous and Authenticated have access. The other roles don't use this. This ended up in solr as
sm_role_access:["administrator"]After I unchecked the "Authenticated" checkbox and enabled every role on its own, the result was as expected (sm_role_access:["administrator", "another role with access", "...."]) and the access for the different roles worked as expected.This seems to be the way Roles work but I found it to be a not-so-desired behavior in this case (unless it's a bug than I misunderstood the concept)
Comment #23
sam152 commentedHm, maybe the authenticated/anonymous role should be manually added to each
UserSession. I looked up some instances ofUserSessionin core and found this in some test code:So that would seem to support the confusing behaviour you saw @DrColossos.
Comment #24
drcolossos commentedFair enough, although this seems like a bug inside the Role system. I tried to look for an issue but couldn't find any.
Anyways, this should be added to the code to make it consistent with the the permission UI tells me.
Comment #25
drcolossos commentedI tried to handle this case: when the role is neither Anonymous nor Authenticated, add the Authenticated role to the user.
Comment #26
sam152 commentedNice patch, but we should add a test case for this additional behavior.
Comment #27
drunken monkeyThanks a lot for testing/reviewing the patch and reporting this problem. However, I’m not certain I understand it completely.
First off, when I tried to add a test for this, I saw that this should already be covered and, thus, work correctly. Yes, when you check “Authenticated” for a permission, only this will get included – so,
role_accesswill be['authenticated'](or['anonymous', 'authenticated'], of course). However, as all users with other roles will have that role, too, that should be all that’s needed.You, however, say the value was just
["administrator"], which doesn’t seem right, then. I think we’ll need further investigation before we maybe just work around a deeper bug we haven’t yet discovered. (Or you realize it was just a specific problem with your setup.)Not sure whether to still leave your fix in or not. On the one hand, it generates additional, unneeded field values. On the other hand, some code might get confused if it needs to handle a
UserSessionobject with neither'anonymous'nor'authenticated'role.However, one more thing I noticed when looking into this, which might actually be related, is that
\Drupal\Core\Session\UserSession::isAuthenticated()doesreturn $this->uid > 0;– so, for our “transient users” we create with negative UIDs, all code using$account->isAuthenticated()will actually think those are just anonymous users. That sounds like it could potentially cause problems, so maybe we should stick with very high UIDs instead after all?I now wrote a short test method for also verifying the actual field values, not just the query results, just to make sure those are correct, too, and we don’t just have too bugs masking each other. At least I could confirm that
'authenticated'works correctly, too, even with negative UIDs. (For now, I reverted your fix and wrote the test accordingly.)Comment #28
drunken monkeyAlso, I just saw this new issue regarding permission cache issues: #3181863: RenderedItem creates users always with uid 0, resulting in incorrect static caching.
As we use a new UID every time, that can’t really happen to us – but we might, on the contrary, want to make sure that the same role always gets the same UID, to make use of caching? (This is meant more as a reminder for me, though.)
Comment #29
sam152 commentedThe
isAuthenticatedimplementation definitely leads me to believe we should be using very high UIDs, instead of negative ones. Nice spotting on that one.I would imagine the only caching that comes into play when calling off to the access API would be static caching, so I can't imagine the impact would be that huge, but it's certainly something that could be optimised now or later.
Nice additional tests too!
Comment #30
drunken monkeyOK, then maybe like this? Any further comments, or RTBC? (Does anyone else want to test, too?)
Comment #32
drunken monkeyOK, I think that was long enough for reviewing.
Committed this now to finally get the functionality in.
Thanks again, everyone!
Comment #34
kalavan commentedHello,
I found problem with watchdog when we use negative or PHP_INT_MAX as uid.
Drupal can't index content after some log message appear.
We should use uid in range: 0 - int(10) to have possibility save logs into db.