Closed (fixed)
Project:
Drupal core
Version:
7.x-dev
Component:
node.module
Priority:
Critical
Category:
Bug report
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
23 Sep 2010 at 14:48 UTC
Updated:
26 May 2011 at 18:12 UTC
Jump to comment: Most recent file
Comments
Comment #1
agentrickardThe core check for access to personal unpublished nodes occurs before Domain Access fires. Check your permissions settings.
If this is not the case, this would seem to be a core bug in node_access().
http://api.drupal.org/api/function/node_access/7
Comment #2
agentrickardCore bug. This is a regression from Drupal 6. The {node_access} table has no knowledge of publication status, so this query needs a condition on View, or we have to re-think how we handle this aspect of the system.
Comment #3
agentrickardMight be that Node Access modules are required to return NODE_ACCESS_IGNORE for the 'view' op if the $node->status = 0.
Comment #4
agentrickardChanging title.
Comment #5
alarfaj commentedThank you agentrickard,
I tested the patch and it works.
Comment #6
agentrickardIt will work, but it likely is the wrong solution, because it won't allow Node Access modules to allow editing / deleting of unpublished nodes.
Comment #7
agentrickardTagging.
Comment #8
chx commentedThis patch kills #452538: Enable Node Grants for Unpublished Nodes
Comment #9
agentrickard@chx
For edit and delete, yes. But we either have to:
a) Extend {node_access} (or $op) to include new grant states.
b) Write in an exception for 'view' in the node_grants subroutine.
You can still access unpublished nodes using hook_node_access(), since the code snippet from #1 occurs first.
Comment #10
agentrickardAnother try.
Comment #11
chx commentedI am studying on how to write a
patchtest for this.Comment #12
chx commented@agentrickard please see the issue I linked. It's definitely desired that node access module control unpublished nodes.
Comment #13
agentrickard'grant_view' is not sufficient to cover the two publication states of a node.
To do that would require an API and schema change, introducing {node_access}.grant_preview, because Node Access modules currently have nothing to query against in the {node_access} table that represents the publication state of the node.
Alternately, we could add a JOIN to the {node} table on the View state here, but doing so will not allow node access modules to control 'preview' grants.
With the API in it's current state, modules have to use hook_node_access() to enable the viewing of unpublished nodes. That happens before Node Access API is invoked.
I am shocked that no one saw this prior, although it came up during Q&A at the Node Access talk Copenhagen, when I mentioned that Node Access modules cannot control access to unpublished nodes.
Comment #14
agentrickardNote: 'preview' == 'view_unpublished'
Comment #15
agentrickardNew patch. This changes the API so that modules can now return three values for hook_node_access_records() and hook_node_grants():
0 == no access
1 == access if node is published
2 == access if node is unpublished
This applies to all 3 $op values (view, update, delete). Good news is that all existing modules currently return 1, so we won't have accidental privilege escalation.
TODO:
1) Tests
2) Documentation
Comment #16
agentrickardThis approach cannot work because hook_node_grants() only returns the grant_ids, not their value.
Comment #17
agentrickardAlternate patch that adds a new {node_access}.view_unpublished column.
Comment #18
agentrickardLet me summarize where we stand. There seem to be four options.
1) Revert to D6 standards, where node access modules have no say for unpublished nodes.
2) Disallow node access modules from responding to 'view' for a _single node_ when it is unpublished. In this case, modules that want to allow this behavior have to use hook_node_access(). This is the approach in #10.
3) Rewrite both hook_node_access_records() and hook_node_grants() to account for both published and unpublished states. This is the approach started in #15.
4) Add a new grant to the node_access table and to hook_node_grants() and hook_node_access_records(). This is the approach in #17, and is actually less intrusive than it sounds, since it would not force changes in contrib.
Personally, I vote for option #2 or #4. The fact of the matter is that $op is broken, and 'view' is simply not a sufficient context for nodes that have a published/unpublished state.
chx's approach in #11 doesn't actually address the issue at hand.
Comment #19
moshe weitzman commentedCan we give a use case that’s hard to solve with current system? AFAICT, node access modules are now fully responsible for granting access. In general, they should not be granting access to unpublished nodes. Thus, they should inspect $node and not return grants if unpublished.
Comment #20
moshe weitzman commentedFor example, if I enable node_access_test.module (use drush to do this), a user still can't see unpublished nodes. That’s because this module doesn't grant access to those. Seems OK to me.
In D7, node access modules have greater powers and need to use them responsibly - be more careful in write_records.
Comment #21
chx commentedSo a documentation issue then?
Comment #22
damien tournoud commentedSo the idea is to make
hook_node_access_records()implementations responsible for checking for$node->status?It seems reasonable to do so, but
node_access_test.moduledoesn't!Comment #23
chx commentedSo #452538: Enable Node Grants for Unpublished Nodes recommended if (!$node->status) { return; } in hook_node_access_records. This is what domain_access does. That does not seem to work too well.
node_access_test module does not test node status at all and so if you have the 'node test view' permission you can view unpublished and published nodes both.
Finally, agentrickard pointed out on IRC that hook_node_grants does not even get $node.
This together makes me think that we have a valid bug to fix not just a docs issue.
Comment #24
damien tournoud commentedOk, so the actual problem is that
node_access_acquire_grants()grants access to a node if no grant is returned from anyhook_node_access_records()implementation:That means that node access implementations have no way to differentiate between "I don't care about this node" and "I care about this node but I don't want anyone to access it".
What I suggest is that we ask node access implementation to return a dummy "deny all" grant like this for that later case:
In case you were wondering
node_access_write_grants()only save positive grants to the database, so those dummy grants will never be saved. They will just preventnode_access_acquire_grants()from granting access to the node to everyone.Comment #25
chx commentedThis is a beginning of a test for what DamZ suggests. It also establishes a very nice easy pattern.
Comment #26
damien tournoud commentedThat makes this mostly a documentation issue.
Here is a suggested change.
Comment #28
moshe weitzman commentedWell, that deny-all in the example is not guaranteed to trump other grants that might have been returned. I suggest you add a high priority to that grant as well.
I don't really get how this handles the 'I don't care' case. If no modules care, then the allow-all grant gets written and that allows anyone to view unpublished.
I think node_access() has to change so it denies unpublished in the 'noone cares' case (even when we are using the grants system).. MW: Jibberish removedComment #29
dave reidSubscribe.
Comment #30
agentrickardI see what DamZ is getting at in #24, but the dummy grant will not be written to the database, due to the logic in
node_access_write_grants(). And setting it for gid 0 is also incorrect. It has to have gid = $node->nid.On the other hand, chx is right and Domain Access does account for 'unpublished' nodes, though not sufficiently, since we probably do want to allow editing and deletion of unpublished nodes.
So the key would be to have core submit a deny grant for unpublished nodes that applies to all node access grants.
The attached patch works as expected when I remove this from domain_node_access_records():
(!$node->status) { return; }. I had that there because I wasn't sure what else to do, and, in fact, I want to allow edit and delete access to unpublished nodes in some cases.This patch to core simply forces grant_view to be set based on $node->status, at the API level.
But I'm still not certain that this will be sufficient to satisfy #452538: Enable Node Grants for Unpublished Nodes, because it will force modules to use hook_node_access_records() to allow access to view unpublished nodes. (Which is still an improvement over D6). The only way to allow this at the {node_access} table level is to alter the API storage.
Comment #31
agentrickardSetting to needs review.
@moshe -- If no modules respond, no record is written for this specific nid, and this bug only occurs when doing a direct query by nid inside the node_access() function.
The argument for the above patch is that setting grant_view on an unpublished node is a security violation, so we cannot trust contributed modules to enforce the logic. We must do so at the API level. Modules that want to override this must explcitly do so in hook_node_access().
Comment #32
damien tournoud commented@agentrickard: the dummy grant will not be saved to the database, but that doesn't matter, at all.
The only non-documentation change required is:
So, there are three use cases:
array('realm' => 'myreal', 'gid' => 3, 'grant_view' => $node->status, 'grant_update' => 0, 'grant_delete' => 0)array('realm' => 'myreal', 'gid' => 3, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0)Comment #34
agentrickard@DamZ
Edited post:
Hm. we'll need to test that extensively.
Original post:
Comment #35
agentrickardNo, the problem is still in case 3 from post #32. In the current system, a node access module does not have enough context to know whether to grant access to an unpublished node. Here's the Domain Access use case:
-- Grants are made not per user, but contextually per domain. So if a node is visible on Domain A, any user can view it on Domain A, regardless of role or permission.
-- We want editors assigned to domain B to be able to edit or delete any node on Domain B, regardless of publication status.
-- We save a grant, grant_view = $node->status, grant_update = 0, grant_delete = 0.
-- This works fine, except that we cannot grant the editor 'view' status of the node without resorting to hook_node_access_records().
-- If we try to save grant_view = 1 to allow access to unpublished nodes, then our invocation of hook_node_grants() has to be smart enough to arbitrate between the two classes of user. But it cannot be, since the grant request has no knowledge of the publication state. So we end up granting access to a class of user that we should not.
One possible solution to that is to pass node status as part of nook_node_grants().
The overall issue is that the ability to apply hook_node_grants() to unpublished nodes essentially means that we have gone from 3 publication states (view, update, delete) to 6 (view, view_unpublished, edit, edit_unpublished, delete, delete_unpublished).
Comment #36
damien tournoud commentedIt's up to the node access module to decide if it wants to give access to unpublished nodes. Generally, most of them will not (domain access clearly will not).
[edit] ie.: you don't need any context. It's a design decision of the node access module to give access to unpublished nodes or not, and to which realm/gid pair to grant that right.
Comment #37
agentrickardSo now you're asking node access modules to create a new realm for every node, just to handle the unpublished state?
Seems like that's a worse solution.
Comment #38
agentrickardDamZ and I hashed this out in IRC. It will be up to the node access module to use a realm to account for 'unpublished.' In most cases, including mine, this will not be done, and unpublished nodes will have grant_view = 0.
Working on documentation now.
Comment #39
agentrickardOK, here's DamZ's patch, with appropriate documentation. Likely needs tests.
Comment #40
agentrickardDang it, let's try that again.
Comment #41
agentrickardAbove patch, with a working test. (Also moves and documents a test that was in the wrong place.)
Comment #43
agentrickardThe test fail is odd, fixed by changing the logic in node_access_acquire_grants(), which may be preferable anyway. Added documentation clarification at chx's suggesting in IRC.
Comment #44
chx commentedI am wondering whether we could do a HEAD / DRUPAL-7--1 module checkout, grep for access_grants and post a critical in their queue to get the maintainer aware.
Comment #45
agentrickardWe can have Randy Fay send a note to the devel list, too.
The test above isn't right, it doesn't check the proper condition. I need to refactor that code.
Comment #46
agentrickardProper test that caught a foreach error when $grants is passed empty to node_access_write_grants().
New test properly leverages the existing node_test.module for node access records testing.
Comment #47
chx commentedsending a word to the devel list about a sechole? is that enough?
Comment #48
agentrickardWell, it's a potential security hole, but only if module developers have the kind of mental block that I had on this issue -- that is, not realizing you need a new realm to handle access to unpublished nodes. We fixed the problem in core, which was the actual security hole.
We don't normally take great strides to notify developers of such things.
This was a bit of a special case for two reasons, I think:
1) I already ported Domain Access to Drupal 7.
2) In that port, I deliberately didn't handle 'unpublished' nodes, since I hadn't figured out what to do about them.
I would expect that other modules may not have similar problems.
Notifying maintainers, though, is a separate issue. Patch still needs review.
Comment #49
catchI'm not very familar with the node access system but the new code and comments read pretty well.
This one is over 80 chars though:
Comment #50
moshe weitzman commentedLooks good. One comment on the docs:
IMO, this 'private' example should check $node->status and simply not return a record at all if unpublished. If this example doesn't care about unpublished (as the comment suggests), then it should stay neutral and let other modules and core sort it out.
Comment #51
agentrickardFixed the inline comment and added clarity to the docblock.
Comment #52
moshe weitzman commentedReady to fly once bot says green
Comment #53
dries commentedBetter to write:
if ($node->status === NODE_PUBLISHED) {?!empty($node->status) means that the node is not stored in the database yet. A node that is stored in the database with status NODE_NOT_PUBLISHED would also be 'unpublished'. Sounds like the code comment might be inaccurate?
Powered by Dreditor.
Comment #54
sunI don't agree with the constant comparison - we don't do that anywhere else.
Not sure whether the other remark is valid. !empty($node->status) does not indicate whether a node is stored in the database or not.
Comment #55
catch$node->status is set via the node form, or via the function calling node_save(), it has no relationship to whether the node is in the database or not. The only two checks for this are $node->is_new and $node->nid.
With the constant comparison, if we wanted to do that, it needs to be in its own code style issue, I don't see any reason to hold this up on either of these, so marking back to RTBC.
Comment #56
dries commentedYou're right. Committed to CVS HEAD. Thanks.
Comment #58
Anticosti commentedSubscribing