The issue only happens when acb module is installed which combines 2 or more node access modules to derive a new node access record and user grants. However, it seems generally a bad practice to user hook_node_access in an access control module as it overrides the behaviour defined via hook_node_access_records() and hook_node_access_grants() in an often unpredictable way.
To be more specific, if a user is not granted a view permission by consulting with node access records, domain_access has some logic which allows access:
function domain_access_node_access(NodeInterface $node, $op, AccountInterface $account) {
...
$manager = \Drupal::service('domain_access.manager');
$allowed = FALSE;
// In order to access update or delete, the user must be able to View.
if ($op == 'view' && $manager->checkEntityAccess($node, $account)) {
/** @var \Drupal\user\UserInterface $user */
if ($node->isPublished()) {
$allowed = TRUE;
}
elseif ($account->hasPermission('view unpublished domain content')) {
$allowed = TRUE;
}
}
This logic is incompatible with acb and potentially other modules that rely purely on the 2 node access canonical hooks mentioned above.
acb has so far implemented special support for domain_access, however I'm not sure if this time acb should adapt again, or it's better for domain_access to take over? It can be classified as a bug, not because it doesn't support acb, but rather because it probably violates the Drupal way of permission management (sorry for being that categorical).
Step to reproduce:
1. Install domain and domain_access and configure
2. Install a module called Permission By Field (pbf) and Access Control Bridge (acb)
3. Configure pbf to use a term field as access control field
4. Make sure for testing the node is created under an allowed domain, but not allowed by PBF settings (i.e. user's term value differs from the node's one so acb will give neutral which is eventually treated as a deny)
5. Access the node and see user can still see it due to $allowed = TRUE; above.
On the same note, the rest of the code in domain_access_node_access doesn't seem to be compatible with pbf's own update/delete permission control set on per-node basis during node creation. Would be good to have these fixed too.
Comments
Comment #2
lex0r commentedComment #3
lex0r commentedComment #4
agentrickardThis is by design of the core API, which is the cause of the issue.
Here, we want to act on unpublished nodes and provide additional granularity for node editing permissions. If some other module returns FALSE, then access should not be granted.
In this case, these would be considered super-permissions, similar to 'edit any page content'.
If you don't want them enforced, simply disable the permissions to use them.
A hook_node_access() definition is only responsible for it's own permissions.
Comment #5
lex0r commented@agentrickard,
This is not quite so, according to the code, as there's another check:
which allows access by just making sure a node is published and user has access to the domain(s) the node belongs to. Isn't it already checked for via core's node access and domain_access's implementation of the two node access hooks? Why do we override it? I would understand your argument if it only applied to
$account->hasPermission('view unpublished domain content')which can be turned off via permissions, but the first condition is defeating the purpose of node access system IMHO.And by "some other module" you mean a custom module, since only custom modules can be made aware of what domain_access is doing and take counter-action? Again, I think this is a wrong approach, and if Drupal core is made this way, we need to initiate a broader discussion on node-access based ecosystem of modules.
I have a feeling something is definitely not right here. A Drupal developer needs a clean way to combine various access control mechanisms relying on node access in a way no overhead is done on every node access check. After all, the whole idea of having "locks and keys" is to avoid unnecessary computations (node access records + hook_node_acces) during run-time and only calculate the run-time value which is user-dependant (i.e. user grants).
Would be good to have acb's, pbf and other node access implementations work nicely together and this is what hasn't been covered and hasn't changed with D8's release, unfortunately.
Comment #6
agentrickardThis part of the code is the one part I agree with you on. It's a tricky question of implementation. Here's the problem as I see it:
* hook_node_access() is designed to override the Node Access system at the page level. Node Access (and modules like DA, ACB, etc.) only work on lists.
* Because hook_node_access works at the individual node level, we can _never_ trust the {node_access} table to return a list of editable content. Core node module breaks this in node_node_access().
* That is a tradeoff between consistency and flexibility.
From the Domain side, to answer the question "Why do we override it?"
* Editors cannot access the edit/delete tabs if they can't View a node. So it makes sense to allow View at the page-level in those cases.
* The ability to edit or delete on a per-domain basis is a legitimate feature that cannot be handled by the Node Access table, since it is tied to permissions. (In theory, we could shove all that in {node_access} but the storage bloat is prohibitive. We can't store per-node-type access records.
Now from here, it's a bit of a question of interpretation:
* I don't think that running two Node Access modules at the same time is ever a good idea. There are too many things that can go wrong because the grants system is OR-based.
* The burden of integration should fall on the integrating module. Individual Node Access modules are complex enough just managing their own permission set.
* (Side note: I maintain Workbench Access as well, which doesn't work on lists at all. It just uses hook_node_access() to sit on top of the core permission set for edit/delete permissions.)
In my ideal world, a module that wanted to combine multiple Node Access schemes would be responsible for the integration code.
My interpretation of the API design is that calling the node grants system inside hook_node_access() is a bad idea. But for modules like ACB that want to enforce this level of granularity, they should provide the integration point.
For ACB that would mean running the Node Access check inside an implementation of hook_node_access(), though you still have to deal with the issue of the permissions set in node_node_access().
All that said, do you have a suggestion for how to solve the View/Edit/Delete dilemma -- aside from adding to grant storage?
Comment #7
agentrickardNote that core doesn't have to deal with the View/Edit/Delete issue because being able to View content is a prerequisite and not overridable within core. See NodeAccessControlHandler::access().
Comment #8
lex0r commented@agentrickard, thanks for replying back, it seems I understand the rationale behind the code in domain_access much better now.
Few considerations anyways:
My interpretation is a bit different. hook_node_access is definitely the only possible way of overriding access control logic, and it is really important. However, the logic is well known and includes 2 steps:
- apply any overrides via hook_node_access
- if no decision has been taken, proceed to the standard node grants check
For performance reason hook_node_access is not called on each node to be shown in a list as the result of a SELECT query, however, it is not a replacement of any kind to node grants check. Both are parts of the same mechanism, and one may expect an identical behaviour regardless whether it was a single node operation (view/update/delete) or a SELECT query.
First of all, I doubt the core node module breaks {node_access} table. This is simply impossible since {node_access} can only be used by a "node access module", i.e. a module like domain_access, acb, pbf etc. node module itself doesn't define any grants apart from the global "anyone can view everything", as it doesn't have the 2 hooks and it's own node_access_grants function is really nothing but a way to alter node grants and add a default one "anyone can view everything". Btw, this function answers the question about how to integrate 2 or more node access modules:
which relates to these lines:
The above comment is a straightforward guideline saying "you are welcome to use node grants in a way to make everyone happy" and it doesn't restrict to hook_node_access usage only.
Why adding to grant storage isn't a good idea?
It is a necessity if one wants to save time and benefit from what this open source world can give!
I'm not really following what you mean here. ACB doesn't have its own implementation of hook_node_access() simply because it doesn't care (and you can't blame it for that!) about what others do with access control, it only operates on grants which is a clean and nice idea IMHO.
I think it all depends on the circumstances. Normally you only use hook_node_access as an "ultimate say" in a decision, in case you either don't have grants defined for some reason, or you want to enable some configuration. I agree however that any custom alterations aimed at "marrying" two node access modules must be done in a custom integration module, but in my ideal world, it should only deal with hook_node_access implementations overrides. The reason is simple: node grants via {node_access} alterations are already covered by ACB.
On a separate note, ACB does have some code to integrate with domain_access which is not in hook_node_access:
Finally, and very important: I see this integration module's task pretty complex if not impossible when it comes to hook_node_access as a way to override undesired behaviour. Let's say domain_access grants access where it shouldn't. The custom module now has to consider this, and "undo" the "allow" by "denying". But what if the custom module doesn't want to deny, it just wants to disable the "allow" by turning it into "I'm neutral" so that {node_access} could be used? Or what if it needs to allow access while it has been denied - no allows will be considered at all if there's at least one deny.
Comment #9
agentrickardThanks for the great reply!
The problem is that I disagree that Domain Access grants access where it should not.
These are super-permissions "edit ANY content on assigned domains". Core has the _exact same issue_ when if comes to "edit ANY {type} content" because that super-permission will trump what's in the node grants.
The only issue that core doesn't have is about View access. That's the one part that your reply doesn't address, and is for me the only real implementation question for Domain Access.
These permissions are designed to override any {node_access} implementation. If that were not the case, then core would run the {node_access} check inside hook_node_access(). It does not. If merely checks permissions. We do the same thing in domain_access_node_access() because those permissions have the same status.
You likely don't see an issue with core Node module simply because the View operation falls through to {node_access} in this implementation. (As I said above, Edit/Delete don't function without View.)
And yes, this is a known flaw in the API design, which values flexibility over consistency. To achieve consistency, ACB would need to check _every_ installed contrib module for a hook_node_access() implementation and understand why access was granted or denied.
My recommendation for ACB is that if you want hook_node_access() to respect the {node_access} table, then ACB should be responsible for doing so. The code for that actually isn't very hard. And you're asking me to put the _exact same logic_ in Domain Access even though it is not needed under a normal use-case.
Essentially, implement hook_node_access() and force it to run the grants lookup for the node, and issue allow/deny solely based on that result.
Three other side notes:
1) The reason I'm against storing per-node-type grants in {node_access} is the problem of storage. The JOIN to {node_access} is already slow because its a varchar index. Adding granular records per node type would explode the storage needs and make that query even slower.
2) Looking at the acb_node_grants() code in isolation, I don't understand why you're involving the permission system at all. You should be able to gather the grants assigned by Domain Access during hook_node_grants_alter() and infer that the permissions are present.
3) The problem with hook_node_access() is that any module might implement it, even one that doesn't use the grants system. This is why it's not possible to generate. an accurate list of editable content.
Comment #10
agentrickardFYI, I'll be at DrupalCON Vienna if you want to discuss in person.
Comment #11
agentrickard