Problem/Motivation
#2830740: Allow workflow types to lock certain changes to workflows once things are in use introduced workflowHasData and workflowStateHasData in order to ensure users were not able to delete workflows or states that had entity moderation data associated with them. With those methods, two things happened:
workflows.modulethen uses these methods to alter the UI of delete forms.content_moderation.moduleimplemented anonConfigImporterValidateevent subscriber to ensure these states and workflows would stick around.
The questions here is, why do these two methods belong on the @WorkflowType interface? They don't really do anything beyond what is already possible with the access system.
Proposed resolution
Attempt 1:
The @WorkflowType plugin already has an access method for states and workflows which allow implementers to restrict deletions, checkWorkflowAccess. Instead of adding two new methods which hold very similar responsibilities to the access method, lets just use the access system for restricting what users have access to.
From an API perspective, users would no longer have to check $workflow->access(...) as well as $workflow->getTypePlugin()->workflowHasData() in order to check if a user really was allowed to delete something.
- Continue to allow users to access the workflow/state delete forms when state/workflow delete "access" is forbidden (as per current behavior in the form of
workflowHasData/workflowStateHasData). - Remove
workflowHasData/workflowStateHasDatafromWorkflowTypeInterface. - Leave these methods on the
ContentModerationplugin, for it's own use internally in::checkWorkflowAccessand beyond.
That ensures the UX is completely unchanged, and this issue becomes an API clean-up.
Attempt 2:
Instead of removing the two methods, why not introduce the semantics in content_moderation (the config validator) into workflows as a feature of the @WorkflowType plugin. That is, use these two methods to check if a workflow or state should be protected from deletion, not because of a users access, but because there is other data associated with those things.
Remaining tasks
Agree.
User interface changes
None.
API changes
Two @internal methods to become @api.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #81 | interdiff_79-81.txt | 6.05 KB | vsujeetkumar |
| #81 | 2897148-81.patch | 36.79 KB | vsujeetkumar |
| #79 | 2897148-78.patch | 33.28 KB | gauravvvv |
| #79 | interdiff-2897148-77_78.txt | 636 bytes | gauravvvv |
| #78 | interdiff_77-78.txt | 1.67 KB | suresh prabhu parkala |
Comments
Comment #2
sam152 commentedComment #4
sam152 commentedUpdating the name of the issue for clarity. This was always going to be a "lets see if this makes more sense".
Comment #5
timmillwood@Sam152 I really liked your suggestions in #2830740: Allow workflow types to lock certain changes to workflows once things are in use and think this would be good, it should not block stable, and should be fine to go in 8.5.x
Comment #6
sam152 commentedHaving a look at this. Agree, based on the methods being @internal, this isn't a blocker.
Comment #7
sam152 commentedHere is what I had in mind for this. The UI and behaviour should be exactly the same. Depending on UX implications we could also allow the required states to reach the delete form, with some explanation that they are essential for the workflow etc.
Comment #8
sam152 commentedComment #10
sam152 commentedFixed tests and issue summary update. One issue with this approach is the docblock on top of \Drupal\Core\Access\AccessResult::forbidden:
While this is intended for devs, it really nicely fits the use case of displaying a message in the UI.
(string) $this->t(...)would probably resolve the issue technically, but would still be considered an abuse?Comment #11
sam152 commentedComment #12
sam152 commentedComment #13
wim leersThis is an API change, which means this would need to happen before the beta. If it happens post-beta, you'll at most be able to mark this
@deprecated.Does that mean this is ?
Comment #14
timmillwoodI think if this doesn't get in before 8.4.0 we update the patch to @deprecate these methods, until then we can assume it will be in 8.4.0 and we can delete these methods.
If that is the direction we want to go, IIRC it was discussed in #2830740: Allow workflow types to lock certain changes to workflows once things are in use that we keep those methods even if we move to an access based solution.
Comment #15
sam152 commentedWe marked these as
@internalin the previous issue so we could take enough time to validate/discuss this.We're also leaving them on the content moderation workflow type, they are used for the config import validator. This patch is purely to change the workflows based UI restrictions to use the access system and to remove the two extra methods from their interface.
Comment #16
timmillwoodThis logic is now a duplicate of what is in
\Drupal\workflows\WorkflowAccessControlHandler::checkAccess.If we had specific a specific "delete workflows" permission I'd question this, but we only have "administer workflows", so I guess it's ok.
So these are now ContentModeration specific methods?
Comment #17
sam152 commented#16.1: The previous issue implemented a UI pattern where you could see the delete links for states which where blocked with
hasDatabut not for the states which were blocked because they were required or if there was one, so this is largely a work-around to say "all these kind of states are blocked from being deleted" and "these special kind of states have their delete button hidden".I think reasonable follow-up to this would be to make it consistent for all states. Ie, for states which are required or if the state is the last one, you get the same UI pattern that denies people access to those states because they have data. That is, you try to delete it and you are given a message as to why you can't. I wanted to keep the UI discussion out of this issue though, hence implementing the work-around to match it exactly 1 for 1.
I'm still not totally sure about the approach, based on the messages in the access results specifically saying "for developer use only", but I'd be interested in exploring why this is and what the ramifications of exposing them are.
Comment #18
sam152 commentedNW based on further investigation from #17.
Also, I think we can safely defer this to 8.5, given the API changes being made are
@internalmethods. Lets focus on the API breaking changes that can't change after stable.Comment #19
wim leers#15: d'oh, I missed that — but that's great news! :)
#16: great points, thanks for the review! On point 2: note that contrib/custom code could alter the access checking for workflows, which means it could introduce additional permissions, which means your point/concern actually still stands.
#18: You'd be right if this was only potentially removing two
@internalmethods. But this patch is also removingWorkflowDeleteAccessCheck, which is a service that is not marked@internal. That change would not be allowed in 8.5, unless it'd be marked@internal. I think you could create a separate issue to mark that class@internal, and add a@todo Remove this service and class in https://www.drupal.org/node/2897148, i.e. refer back to this issue. Then this issue can safely remove it, or if it ends up changing direction, it could remove the@internal.Comment #20
sam152 commented#19 sounds very sensible. Opened #2900634: Mark WorkflowDeleteAccessCheck @internal until a UI pattern for access-restricted states has been established to cover this.
Comment #21
wim leers👍
RTBC'd.
Comment #22
wim leersComment #23
larowlanBlocker has been resolved
Comment #24
sam152 commentedHaving stewed on this for a few days, I've got some more thoughts. My initial reaction for thinking this was so important was because from the perspective of the workflows module in isolation, implementing these methods doesn't add any guarantees beyond what the state/workflow access checks already give you. They only stop you from deleting the state/workflow from the UI, something access already handles nicely.
The alternative to this is to remove the
@internalfrom these methods but also move\Drupal\content_moderation\EventSubscriber\ConfigImportSubscriberto workflows. That way the methods have some good utility on theWorkflowTypeinterface, they protect the integrity of your content/configuration. In a few of the use-cases I have in my head, this would actually be a kinda useful feature.I've also just noticed
ConfigImportSubscriberin content_moderation doesn't actually check the workflow type, so when enabled it actually provides this level protection to all workflow types (helpful right?). Also, the code in here is really non-trivial, meaning we have more of a reason to support these methods being part of the workflow type plugin, as reproducing the functionality would be a total headache.Sorry for the total 180 on this issue. I'm going to see what the above looks like in a patch.
Comment #25
amateescu commented@Sam152, I'm glad you changed your mind about the general usefulness of these methods :) Here's another place where we need them: #2904101: Do not allow modules that provide a workflow type plugin with existing data to be uninstalled
Comment #26
wim leersFor #25.
Comment #27
wim leersI'm not sure I fully understand #24. Is it saying that rather than using the entity access system, it should use a
\Drupal\Core\Config\ConfigEvents::IMPORT_VALIDATEevent subscriber?Perhaps it's because I've been out of Drupal for a week, but I'm really having a hard time grokking #24 — I'd appreciate it if you could elaborate. Or perhaps update the IS with your new thinking (while keeping the old thinking mentioned and explaining your reasons for changing — this also helps core committers, see #2824851-107: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior for example).
Comment #28
sam152 commentedRe: #27 maybe some of the understanding is wrapped up in #2830740: Allow workflow types to lock certain changes to workflows once things are in use.
content_moderationalready introduced the event subscriber and theworkflowStateHasData/workflowHasDatamethods in order protect states and workflows from being deleted when there was moderated content associated with them. If you look at the patch from the linked issue and only show theworkflows.modulepart (attached), all it does with the additional methods is wrap some of the UI elements to make sure they cannot be deleted. The reason I opened this issue is because the access system already does exactly this, so as an implementor there'd be no real difference between adding some access checks and implementing these hasData methods.The alternative is provide the same feature
content_moderationimplements, config validation to ensure these states and workflows cannot be removed, but for all@WorkflowTypes that implement those methods. Hence they become useful beyond what you could accomplish with access control and could form part of the public API with some valid distinction between the access system already in place.Hope that makes some more sense.
Comment #29
timmillwoodI think it makes sense to move the config import event subscriber to Workflows, I'm not sure why it was put in Content Moderation in the first place.
Comment #30
sam152 commentedAs per #27, the IS needs some serious finessing. I'll take a look at that in the morning.
Here is the start of a patch. I think
ContentModerationWorkflowConfigTestcan and should be replaced entirely with a much simpler test that asserts the return values ofworkflowHasData/workflowStateHasDatabut for now at least, it's useful to make sure the behaviour didn't change moving this from CM to workflows.Comment #31
sam152 commentedReroll.
Comment #32
sam152 commentedHere is an IS update which covers the two trains of thought.
Comment #33
sam152 commentedComment #34
sam152 commentedComment #35
wim leers#28: thank you, that helped a lot!
So we're officially making these non-internal. Makes sense. But can we then add
@see \Drupal\workflows\EventSubscriber\ConfigImportSubscriber, to make it clear what this being used for?Let's make these strings constants. Will make the test more robust.
So the patch in #31 is now just moving the config import validation from
content_moderationtoworkflowsand adding test coverage.BUT…
That this error message is still UI/form/render-specific.
Therefore when this action is attempted to be performed via REST in the future, it won't get a similar error message.
This is why it's wrong/problematic from an API-First point of view to have error checking + error messages hardcoded in the UI rather than in the underlying logic.
IOW: in HEAD, business logic is coupled to UI logic, this change is decoupling them, allowing the same logic to also be applied for API-First purposes.
Therefore I think we should also still do what the patches prior to the new approach were doing.
Sorry!
Comment #36
sam152 commentedThanks for the review. I'm glad these are
@internaland we can take our time to sort it out. It seems like we need some combination of the two, REST support was one of the reasons I was pushing for the original solution, so it only makes sense to include it as well.Comment #37
wim leersExactly! :)
Comment #38
amateescu commentedRe #35:
These methods are also used in
</code> and #2904101: Do not allow modules that provide a workflow type plugin with existing data to be uninstalled adds another usage. Do we really need to add <code>@seeto every usage of a public method? And if not, how do we determine which usages are more important than others?I don't agree with this.. We use this pattern of state set/get in dozens of test and I've never seen any of them using constants..
As for the patch itself, am I the only one who thinks it's weird that both of these methods receive a workflow entity as their first argument? I would have expected to have that passed-in automatically somehow..
Comment #39
sam152 commentedThese are the only methods which are really concerned about the thing-being-moderated directly, the rest of the methods mostly just provide details around what's possible in the workflow. I suppose if a plugin was instantiated somehow without the workflow entity as the storage, these wouldn't make much sense.
Having a look at the feedback and access controller now.
Comment #40
sam152 commentedAdding the access control back in and addressing some of the feedback.
The tests that assert you're allowed to visit the "delete" pages of the state and workflow, even if you aren't allowed to delete them will fail. Not sure how to deal with that yet, revert the UI pattern or emulate it by removing all the access off those routes.
Comment #41
sam152 commentedFixing a bug and adding a test case for it.
The reason this test isn't failing is because it doesn't assert the warning message about the state/workflow having data exists for this page. It only asserts there is no button, which is also TRUE for a 403.
Comment #42
sam152 commentedI wonder if fixing the test should be a seperate issue, so it doesn't look like this patch goes to great lengths to introduce a new UI pattern when it actually already exists?
Comment #43
sam152 commentedOpened #2910715: The assertions in ModerationFormTest::testWorkflowInUse are inadequate to keep this issue focused. Will retest #41 once that's in.
Comment #44
sam152 commentedThis should now fail.
Comment #46
sam152 commentedComment #47
sam152 commentedFixing the test.
Comment #48
sam152 commentedThis is the last thing I'm unsure about. These decisions are based on calls to hasData methods. We don't know what cacheability exists behind the checks in these methods, so unless we were able to return metadata with these methods themselves, the whole access check is uncacheable.
Comment #49
sam152 commentedComment #51
sam152 commentedFixing some more tests. Required states are still access denied, but the page can now be visited just like states with data. We still don't show the "Delete" link on the workflows UI, so from a users perspective the UI is the same before and after this patch.
Comment #52
sam152 commentedNW for #48.
Comment #53
sam152 commentedComment #54
wim leers#48
\Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowHasData()and\Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowStateHasData()both do entity queries, and then return a boolean. What's missing here, are entity list cache tags — in this case:content_moderation_state_list.This cache tag is technically associated with the boolean that is returned: the boolean might change whenever the list cache tag is invalidated. Which means that technically, the return value should not be
boolbutCacheableBool. With:but doing that would change the signature of
\Drupal\workflows\WorkflowTypeInterface::workflowHas(State)Data, and hence cause a BC break.So you have two options:
\Drupal\Core\Access\AccessibleInterface::access(): add an optional$return_as_object = FALSEparameter, which by default returns just the boolean, and if the caller opts in, returns the boolean plus cacheability metadata.Comment #55
sam152 commentedRe: #54,
CacheableBoolis exactly what we need, thank you! Is this something that already exists or would this be the first usecase for it?Hard coding the list cache tags is out of the question because
workflow(State)HasDatamethods on a plugin interface and can thus will be based on various other things for different workflow types.The good news is, these methods are
@internal, so we can safely change the signature as required.Comment #56
sam152 commentedOn alternative I can also see is to pass in some param, which the user could then add stuff to and not have to create an object to return:
public function workflowHasData(WorkflowInterface $workflow, CacheableMetadata $metadata);I suppose it'd be similar to
BubbleableMetadatabeing passed tohook_tokensfor example. I do worry that if this issue introducesCacheableBoolwhich is only required/integrated with these methods, it could be really confusing for the rest of the Drupal. For example why wouldn't a factory onAccessResultsupport passing aCacheableBoolinstead of a primitive? What utility is expected out ofCacheableBoolas a return type for cache-related things.Comment #57
sam152 commentedImplementing #56 to see what that looks like. Reviews very much welcome :)
Comment #58
sam152 commentedRestoring the links to the workflow/state delete forms so this issue doesn't introduce any UI changes.
Comment #59
sam152 commentedDrafted a change record for this: Workflow type plugins can now implement the workflowHasData and workflowStateHasData methods to protect the integrity of a workflow.
Comment #60
timmillwoodThere's a few coding standards issues as mentioned in https://www.drupal.org/pift-ci-job/790541, but once done I think we're ready for RTBC.
Assigning to Sam152 to complete the updates.
Comment #61
sam152 commentedFixing.
Comment #62
timmillwoodThanks @sam152!
Comment #63
sam152 commentedWoohoo! Thanks for the review @timmillwood.
Comment #64
larowlanWe should use the third arg for in_array here because these are strings
This is an API break on a non-internal class in a stable module, so we need to support passing NULL unfortunately. And we need tests for that too.
I prefer returning early over if/elseif/else.
I think that will make this tidier and easier to read
Comment #65
larowlanComment #66
sam152 commented1. Fixed.
2. Both the changed methods are currently marked @internal, is that enough here?
3. Refactored into early returns. Added a comment to a part of this that became a little less clear.
Comment #67
larowlan2. Ah sorry, I missed that the method was internal, I only checked the class - all good.
Comment #69
sam152 commentedI think I'll try pick this back up as soon as #2896726: Expand the entity access model for workflow states and transitions. is in. The two conflict quite heavily, but I think it would be great integrity features CM adds to all workflow types.
Comment #76
kristen polPatch does not apply to 9.3:
Comment #77
ankithashettyRe-rolled the patch in #66 and attached an diff file.
Thanks!
Comment #78
suresh prabhu parkala commentedTried to fix custom failures in the patch #77. Please review.
Comment #79
gauravvvv commentedRe-rolled patch #77, fixed cs error.
Comment #81
vsujeetkumar commentedFixed fail tests, Please have a look.