Skip to content

Commit

Permalink
Merge aa99141 into 68a00da
Browse files Browse the repository at this point in the history
  • Loading branch information
deiferni committed Oct 7, 2020
2 parents 68a00da + aa99141 commit d713da5
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 17 deletions.
2 changes: 2 additions & 0 deletions news/999.bugfix
@@ -0,0 +1,2 @@
Fix ``@workflow`` when executing user has no permissions to access ``review_history`` in target state.
[deiferni]
9 changes: 9 additions & 0 deletions src/plone/restapi/configure.zcml
Expand Up @@ -38,6 +38,15 @@
for="Products.CMFPlone.interfaces.ITestCasePloneSiteRoot"
/>

<genericsetup:registerProfile
name="testing-workflows"
title="plone.restapi testing-workflows"
directory="profiles/testing-workflows"
description="Adds sample workflows for testing"
provides="Products.GenericSetup.interfaces.EXTENSION"
for="Products.CMFPlone.interfaces.ITestCasePloneSiteRoot"
/>

<genericsetup:registerProfile
name="performance"
title="plone.restapi performance testing"
Expand Down
@@ -1,5 +1,6 @@
<?xml version="1.0"?>
<object name="portal_workflow" meta_type="Plone Workflow Tool">
<object name="restriction_workflow" meta_type="Workflow"/>
<bindings>
<type type_id="DXTestDocument">
<bound-workflow workflow_id="simple_publication_workflow" />
Expand Down
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<dc-workflow workflow_id="restriction_workflow" title="Workflow for testing" description=" - Workflow that cover some special cases for testing. - Has two states: Visible and Restricted." state_variable="review_state" initial_state="unrestricted" manager_bypass="False">
<permission>Access contents information</permission>
<permission>Modify portal content</permission>
<permission>View</permission>
<state state_id="restricted" title="Restricted">
<description>Restricted for Admins.</description>
<exit-transition transition_id="show"/>
<permission-map name="Access contents information" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Site Administrator</permission-role>
</permission-map>
<permission-map name="Modify portal content" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Site Administrator</permission-role>
</permission-map>
<permission-map name="View" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Site Administrator</permission-role>
</permission-map>
</state>
<state state_id="unrestricted" title="Unrestricted">
<description>Unrestricted visibility.</description>
<exit-transition transition_id="restrict"/>
<permission-map name="Access contents information" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Owner</permission-role>
<permission-role>Editor</permission-role>
<permission-role>Reader</permission-role>
<permission-role>Contributor</permission-role>
<permission-role>Member</permission-role>
<permission-role>Site Administrator</permission-role>
</permission-map>
<permission-map name="Modify portal content" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Owner</permission-role>
<permission-role>Editor</permission-role>
<permission-role>Site Administrator</permission-role>
</permission-map>
<permission-map name="View" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Owner</permission-role>
<permission-role>Editor</permission-role>
<permission-role>Reader</permission-role>
<permission-role>Contributor</permission-role>
<permission-role>Member</permission-role>
<permission-role>Site Administrator</permission-role>
</permission-map>
</state>
<transition transition_id="restrict" title="Restrict visibility" new_state="restricted" trigger="USER" before_script="" after_script="">
<action url="%(content_url)s/content_status_modify?workflow_action=restrict" category="workflow" icon="">Restrict</action>
<guard>
<guard-permission>Modify portal content</guard-permission>
</guard>
</transition>
<transition transition_id="show" title="Show content" new_state="unrestricted" trigger="USER" before_script="" after_script="">
<action url="%(content_url)s/content_status_modify?workflow_action=show" category="workflow" icon="">Show</action>
<guard>
<guard-permission>Modify portal content</guard-permission>
</guard>
</transition>
<variable variable_id="action" for_catalog="False" for_status="True" update_always="True">
<description>Previous transition</description>
<default>

<expression>transition/getId|nothing</expression>
</default>
<guard>
</guard>
</variable>
<variable variable_id="actor" for_catalog="False" for_status="True" update_always="True">
<description>The ID of the user who performed the last transition</description>
<default>

<expression>user/getId</expression>
</default>
<guard>
</guard>
</variable>
<variable variable_id="comments" for_catalog="False" for_status="True" update_always="True">
<description>Comment about the last transition</description>
<default>

<expression>python:state_change.kwargs.get('comment', '')</expression>
</default>
<guard>
</guard>
</variable>
<variable variable_id="review_history" for_catalog="False" for_status="False" update_always="False">
<description>Provides access to workflow history</description>
<default>

<expression>state_change/getHistory</expression>
</default>
<guard>
<guard-permission>View</guard-permission>
</guard>
</variable>
<variable variable_id="time" for_catalog="False" for_status="True" update_always="True">
<description>When the previous transition was performed</description>
<default>

<expression>state_change/getDateTime</expression>
</default>
<guard>
</guard>
</variable>
</dc-workflow>
55 changes: 44 additions & 11 deletions src/plone/restapi/services/workflow/transition.py
@@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-

from AccessControl import getSecurityManager
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import setSecurityManager
from AccessControl.User import UnrestrictedUser as BaseUnrestrictedUser
from DateTime import DateTime
from plone.restapi.deserializer import json_body
from plone.restapi.interfaces import IDeserializeFromJson
Expand All @@ -9,6 +13,7 @@
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowException
from zExceptions import BadRequest
from zope.component import getMultiAdapter
from zope.component import queryMultiAdapter
from zope.i18n import translate
from zope.interface import alsoProvides
Expand All @@ -20,6 +25,14 @@
import six


class UnrestrictedUser(BaseUnrestrictedUser):
"""Unrestricted user that still has an id."""

def getId(self):
"""Return the ID of the user."""
return self.getUserName()


@implementer(IPublishTraverse)
class WorkflowTransition(Service):
"""Trigger workflow transition"""
Expand Down Expand Up @@ -77,20 +90,40 @@ def reply(self):
self.request.response.setStatus(400)
return dict(error=dict(type="Bad Request", message=str(e)))

history = self.wftool.getInfoFor(self.context, "review_history")
action = history[-1]
if six.PY2:
action["title"] = self.context.translate(
self.wftool.getTitleForStateOnType(
action["review_state"], self.context.portal_type
).decode("utf8")
sm = getSecurityManager()
portal = getMultiAdapter(
(self.context, self.request), name="plone_portal_state"
).portal()
try:
tmp_user = UnrestrictedUser(
sm.getUser().getId(), "", ("manage", "Manager"), ""
)
else:
action["title"] = self.context.translate(
self.wftool.getTitleForStateOnType(
action["review_state"], self.context.portal_type
tmp_user = tmp_user.__of__(portal.acl_users)
newSecurityManager(None, tmp_user)
history = self.wftool.getInfoFor(self.context, "review_history")
action = history[-1]
if six.PY2:
action["title"] = self.context.translate(
self.wftool.getTitleForStateOnType(
action["review_state"], self.context.portal_type
).decode("utf8")
)
else:
action["title"] = self.context.translate(
self.wftool.getTitleForStateOnType(
action["review_state"], self.context.portal_type
)
)
except WorkflowException as e:
self.request.response.setStatus(400)
action = dict(
error=dict(
type="WorkflowException",
message=translate(str(e), context=self.request),
)
)
finally:
setSecurityManager(sm)

return json_compatible(action)

Expand Down
3 changes: 2 additions & 1 deletion src/plone/restapi/setuphandlers.py
Expand Up @@ -15,9 +15,10 @@ class HiddenProfiles(object):
def getNonInstallableProfiles(self): # pragma: no cover
"""Do not show on Plone's list of installable profiles."""
return [
u"plone.restapi:blocks",
u"plone.restapi:performance",
u"plone.restapi:testing",
u"plone.restapi:blocks",
u"plone.restapi:testing-workflows",
u"plone.restapi:uninstall",
]

Expand Down
15 changes: 15 additions & 0 deletions src/plone/restapi/testing.py
Expand Up @@ -200,6 +200,21 @@ def setUpPloneSite(self, portal):
)


class PloneRestApiTestWorkflowsLayer(PloneSandboxLayer):

defaultBases = (PLONE_RESTAPI_DX_FIXTURE,)

def setUpPloneSite(self, portal):
applyProfile(portal, "plone.restapi:testing-workflows")


PLONE_RESTAPI_WORKFLOWS_FIXTURE = PloneRestApiTestWorkflowsLayer()
PLONE_RESTAPI_WORKFLOWS_INTEGRATION_TESTING = IntegrationTesting(
bases=(PLONE_RESTAPI_WORKFLOWS_FIXTURE,),
name="PloneRestApiTestWorkflowsLayer:Integration",
)


class PloneRestApiDXPAMLayer(PloneSandboxLayer):

defaultBases = (DATE_TIME_FIXTURE, PLONE_APP_CONTENTTYPES_FIXTURE)
Expand Down
29 changes: 24 additions & 5 deletions src/plone/restapi/tests/test_workflow.py
Expand Up @@ -7,8 +7,9 @@
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import TEST_USER_PASSWORD
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
from plone.restapi.testing import PLONE_RESTAPI_WORKFLOWS_INTEGRATION_TESTING
from Products.CMFCore.utils import getToolByName
from unittest import TestCase
from zExceptions import NotFound
Expand All @@ -19,7 +20,7 @@

class TestWorkflowInfo(TestCase):

layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
layer = PLONE_RESTAPI_WORKFLOWS_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
Expand Down Expand Up @@ -99,7 +100,7 @@ def test_workflow_info_empty_on_siteroot(self):

class TestWorkflowTransition(TestCase):

layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
layer = PLONE_RESTAPI_WORKFLOWS_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
Expand All @@ -108,13 +109,16 @@ def setUp(self):
login(self.portal, SITE_OWNER_NAME)
self.portal.invokeFactory("Document", id="doc1")

def traverse(self, path="/plone", accept="application/json", method="POST"):
def traverse(
self, path="/plone", accept="application/json", method="POST", auth=None
):
request = self.layer["request"]
request.environ["PATH_INFO"] = path
request.environ["PATH_TRANSLATED"] = path
request.environ["HTTP_ACCEPT"] = accept
request.environ["REQUEST_METHOD"] = method
auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
if auth is None:
auth = "%s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
request._auth = "Basic %s" % b64encode(auth.encode("utf8")).decode("utf8")
notify(PubStart(request))
return request.traverse(path)
Expand Down Expand Up @@ -192,3 +196,18 @@ def test_invalid_effective_date_results_in_400(self):
res = service.reply()
self.assertEqual(400, self.request.response.getStatus())
self.assertEqual("Bad Request", res["error"]["type"])

def test_transition_with_no_access_to_review_history_in_target_state(self):
self.wftool.setChainForPortalTypes(["Folder"], "restriction_workflow")
self.portal[self.portal.invokeFactory("Folder", id="folder", title="Test")]
setRoles(
self.portal, TEST_USER_ID, ["Contributor", "Editor", "Member", "Reviewer"]
)
login(self.portal, TEST_USER_NAME)

auth = "%s:%s" % (TEST_USER_NAME, TEST_USER_PASSWORD)
service = self.traverse("/plone/folder/@workflow/restrict", auth=auth)
res = service.reply()

self.assertEqual(200, self.request.response.getStatus(), res)
self.assertEqual(u"restricted", res[u"review_state"], res)

0 comments on commit d713da5

Please sign in to comment.