Problem/Motivation
Created as a follow-up to #2350939: Implement a generic revision UI.
There are several problems that result from the Revert and Delete actions being available for the current revision.
- The user interface break slightly, with the Revert/Delete split/drop button being directly to the right of the "Current revision" text, all under the single "Operations" column.
- Following through the Revert action technically works, in that a new "revert" revision is created, practically nothing has changed because the "current revision" is the current state of the content. In other words, reverting the current revision is illogical because there is nothing to revert to.
- Following through the Delete action results in an error and White Screen of Death, which leaves the entity in a broken and unrecoverable state.

The current solution to this is that every entity implementing the Generic Revision UI must include logic in its own access checker to ensure that the Revert and Delete actions cannot be accessed. Example from #1984588: Add Block Content revision UI:
'revert' => AccessResult::allowedIfHasPermissions($account, [
'administer blocks',
'revert any ' . $bundle . ' block content revisions',
], 'OR')->orIf(AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision())),
'delete revision' => AccessResult::allowedIfHasPermissions($account, [
'administer blocks',
'delete any ' . $bundle . ' block content revisions',
], 'OR')->orIf(AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision())),
In short, each implementation must include something to the effect of AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision() in the access checker, once for the revert operation, and a second time for the delete revision operation.
There are two issues here:
- It's reasonable to assume that some developers will forget or not realise they need to include this in their access checkers, resulting in some implementation potentially being buggy.
- The fact that every implementation needs to include what is in effect the same lines of code suggests that overall developer effort would be reduced by providing this logic in an access checker out of the box, as well as lowering the barrier to entry and reducing the likelihood of bugs in contrib/custom implementations.
Proposed resolution
Remove isDefaultRevision and isLatestRevision checks from entity type specific access control handlers (e.g BlockContentAccessControlHandler)
Add check for isDefaultRevision for revert and delete revision operations in generic EntityAccessControlHandler and return a forbidden
Improve and fix test coverage for these operations, including expanding EntityTest access tests.
Remaining tasks
Final review
| Comment | File | Size | Author |
|---|---|---|---|
| #37 | 3323788-37-test-only.patch | 1.45 KB | lauriii |
| #24 | Screenshot from 2023-07-19 15-17-14.png | 34.58 KB | berdir |
| Screenshot 2022-10-25 110354.jpg | 21.36 KB | aaronmchale |
Issue fork drupal-3323788
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3323788-revert-and-delete
changes, plain diff MR !4359
Comments
Comment #2
aaronmchaleIt would probably help everyone if we could resolve this prior to 10.1 release.
Quoting myself from #2350939-307: Implement a generic revision UI:
Comment #4
berdirOne note from a related slack thread, I'm not sure if an (only) access-based implementation for this is really the right thing. It's not really about access, it's about data consistency.
\Drupal\node\Controller\NodeController::revisionOverview() handles that directly in there, and I think the default implementation could too. That said, I suppose (also?) implementing it as an access check could help with exposed additional interfaces like jsonapi or something..
Additionally, it probably makes sense to throw an exception on the API level if trying to do it anyway.
Comment #5
yash.rode commentedImplementing #2 as this is blocking #2911977: Add Media revision UI
Comment #7
yash.rode commentedI implemented the approach discussed in #2, and it will work without any BC breaks, to test it I used the newly created helper class for block content. If we don't want to rely on access based solution as stated in #4 I will need some help with how to implement that.
Comment #8
yash.rode commentedComment #9
smustgrave commentedThis will need test coverage, as previously tagged.
But tried replicating on 11.x with nodes and content blocks and I don't see the revert or delete for the current revision without using the patch. So adding steps to reproduce.
Comment #10
yash.rode commentedSteps to reproduce:
1. with the current patch applied, In
\Drupal\block_content\BlockContentAccessControlHandlerextendEntityAccessControlHandlerinstead ofEntityAccessControlHandlerWithRevision.2. You should be able to see the revert and delete button for recent revision for block.
3. Now revert this change and go back to the initial stage with just the patch applied.
4. you should not see the revert and delete button for latest revision.
I think
\Drupal\Tests\block_content\Functional\BlockContentRevisionRevertTest::testRevertFormand
\Drupal\Tests\block_content\Functional\BlockContentRevisionDeleteTest::testDeleteFormalready provides test coverage for this. So I am not sure if we need a separate test.Comment #11
yash.rode commentedDescribed this in #10
Comment #12
smustgrave commentedSo this issue currently doesn't exist unless it's setup?
1. with the current patch applied, In \Drupal\block_content\BlockContentAccessControlHandler extend EntityAccessControlHandler instead of EntityAccessControlHandlerWithRevision.
Comment #13
yash.rode commentedNo, because we already have
AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision()in\Drupal\block_content\BlockContentAccessControlHandler::checkAccess, and we want to remove it from there.Comment #14
phenaproximaI suggest we do this, as suggested by #2:
To me, this would reduce API surface and code duplication, and also protect all entity types from the data integrity issues caused by this bug. That might technically be considered a behavior-breaking BC change, but if the current behavior is capable of breaking entities and making them unusable, that's data loss and probably a sufficient justification for fixing the broken behavior.
Comment #15
yash.rode commentedI think it should be fine to do that in `access()` for two reasons.
1. We need to call the `checkAccess()` method at the start of every override of `checkAccess()` and check the returned result is forbidden, if yes, then return forbidden at the start. But we want to do that in the parent class.
2. We are skipping the caching stuff only for the case where it is latest and default for delete or revert operation, in which case it's anyway going to be forbidden. So, I am not sure if that case is going to be a problem.
Comment #16
berdirYeah, checkAccess is usually not calling the parent and calling it and only respecting forbidden is very arbitrary.
I'm still not convinced that this is logic that should be done through the access API. At least we should additionally prevent this with an exception directly in the storage. See \Drupal\Core\Entity\EntityStorageBase::doPreSave() and \Drupal\Core\Entity\ContentEntityStorageBase::doPreSave() for existing examples that through exceptions if a save (or delete revision in this case) operation would cause data integrity issues.
This does not need extra test coverage IMHO, it's a refactoring of an existing check and already tested as the test fails in https://www.drupal.org/pift-ci-job/2714026 show.
Comment #17
yash.rode commentedAddressed @Berdir's feedback and test fix.
Comment #18
berdirReviewed, this is not correct indeed. Not sure if my feedback was misunderstood, because what I asked for does in fact already exist and does not require a change. The tests added/changed here are either incomplete or incorrect about the default revision behavior if there are forward revisions.
The notes about missing/bogus test coverage around node revisions and the deleteRevision() check are just documenting my findings as I was going through this, it doesn't need to be done here. But it might make sense to have a follow-up to add better test coverage for deleteRevision() to avoid breaking this in the future.
For the node revision UI, we already have an issue to use the new generic code, which I guess will then also cover to unify node with block_content/media.
Comment #19
yash.rode commentedReverted the change and created a follow-up.
Comment #20
berdirThe access check still has the same incorrect default AND latest check that is not correct. Since this is new code, we should make sure that is covered here with tests.
Comment #21
yash.rode commentedIf we remove that default AND latest check, how are we going to achieve what we want to do in this issue? Or we want it to be just default?
Comment #22
berdir> Or we want it to be just default?
Yes, this.
I messed up my formatting, but what I wrote previously is
> The same can also easily be done in the UI on the MR, and you get the delete/revert operations next to the current revision message.
So install content_moderation, enable it for custom blocks, then create a draft of a published block, then it must not be possible to delete the default revision.
And then either a UI or a kernel test (which doesn't need content moderation, it just needs to create an entity with setnewRevision(TRUE) + isDefaultRevision(FALSE)) where you verify that. Very much like the node issue you created, so that keeps working once we switch node to this.
Comment #23
yash.rode commentedI have changed it to just check default and on creating a draft of published custom block. I can see the revert and delete button, but I am not able to delete the published revision. That's what we are expecting, right? If yes, I will continue to write the test.
Comment #24
berdirYou should see delete and revert for the draft, but not the current revision, yes. Testing manually, I think this is also broken on blocks in HEAD, I guess you just moved that logic. If you compare it with node, it's different (and node is doing it correctly)
Comment #25
yash.rode commentedSorry, It is working as expected now I had missed something. Working on test.
Comment #26
narendrarBoth functional tests looks good to me.
Q: Does this change require a CR?
Comment #27
berdirI think this is ready now.
Some notes/summary:
* I'm changing it to a bug, because it's not just making the existing check in block_content generic, it's fixing it as well as it is currently wrong.
* Still feels a bit strange that the logic for this is part of access checking, but we have the API level check as well where we throw an exception (but no existing tests for that, but that's not in scope). The advantage of doing it in access means we kind of have an API for this that for example jsonapi could check too.
* Node does this in its controller currently, but that will change when node starts using the generic UI.
* The force revert stuff was bogus, even without the access check, you can't produce a data consistency issue due to the exception. I think that's why this is only a normal bugfix and not major. You could argue that the test coverage for it should be in the generic test entity type and not block_content, but I don't feel so strongly about it.
* I was confused about the time change in the delete confirmation messages, but that's because we now delete a different revision, before it was a previous revision, now it's latest.
Comment #29
acbramley commentedCaught a bug while testing this, that obviously needs test coverage (checking for revert or delete revision access when $return_as_object is FALSE)
Comment #30
yash.rode commentedAdded test coverage.
Comment #31
aaronmchaleI think it does make more sense for the test coverage to be in the generic test entities. Having the test coverage in the block_content module for something which is being made a generic implementation feels weird.
Comment #32
smustgrave commentedCatching up and reading the comments seems like the consensus is to move those tests to be generic.
If there's an issue with block_content should that be fixed first before this?
Comment #33
yash.rode commentedI think we can rely on this test coverage because we will be adding similar tests for node and media also after they start using generic UI. Anyway, we can't skip test coverage for node, media, and block content, so if we write a test for generic it will get repeated.
If there is any strong reason for writing tests in generic, we can do that.
Comment #34
yash.rode commentedTesting for generic.
Comment #35
yash.rode commentedWe already have the test coverage for this in generic,
\Drupal\FunctionalTests\Entity\RevisionDeleteFormTest::testAccessDeleteDefaultand\Drupal\FunctionalTests\Entity\RevisionRevertFormTest. The tests were already passing because of\Drupal\Core\Entity\ContentEntityStorageBase::deleteRevisionWhich hadisDefault()check on database level. To check, we can comment this part as well as comment the part we added in\Drupal\Core\Entity\EntityAccessControlHandler::access--\Drupal\FunctionalTests\Entity\RevisionDeleteFormTest::testAccessDeleteDefaultshould fail and uncommenting the part we added in this issue should pass the test.Other than that, I think we should keep the changes to the block content's test, according to #3375168: Improve test coverage around node revisions and the deleteRevision().
Comment #36
acbramley commentedGreat work @yash.rode this is ready to go again.
Comment #37
lauriiiI think it would be great to also get an issue summary update that describes the bug we are fixing, and the changes we are making.
If we need to make changes to code outside of the changes introduced here, it sounds like that the test coverage might not be sufficient. Since we are introducing new access controls to the generic entity access control handler, it should be tested outside of the implementations.
We do have pre-existing test coverage for the API itself in a test entity type. Couldn't we utilize these by removing$entity->isDefaultRevision()check from\Drupal\entity_test\EntityTestAccessControlHandler::checkAccessto make it rely on the generic access control handler? It looks like\Drupal\entity_test\EntityTestAccessControlHandler::checkAccessis preventing.Looks like the test coverage introduced in #2350939: Implement a generic revision UI has a mistake in a critical part which makes it fail to test what it's supposed to 🙈 I'll work on this a bit. Here's a patch that should fail.
Comment #38
acbramley commentedNew tests look good, IS is updated.
Nice catch on that test bug!
Comment #39
lauriiiLooks like I forgot to unassign this. Thanks for the issue summary update and for moving this to needs review @acbramley!
Comment #40
smustgrave commentedIssue summary reads clear now.
Think this is ready!
Comment #42
lauriiiCommitted 117e716 and pushed to 11.x. Thanks!
Technically, I think this could be backported but it's probably safer to do this in a minor only due to the changes in the access control handler.
Comment #45
amateescu commentedFolks who worked on this issue might be interested in this followup: #3440304: The default revision can not be reverted (to) when there are pending revisions