Added contributors restrictions to allow a contributor to manage contents owned by other principals
authorThierry Florac <tflorac@ulthar.net>
Tue, 01 Oct 2019 12:04:20 +0200
changeset 1358 c682811fa1ea
parent 1357 329116e5f8e3
child 1359 a28a08bb2d4e
Added contributors restrictions to allow a contributor to manage contents owned by other principals
src/pyams_content/shared/common/interfaces/__init__.py
src/pyams_content/shared/common/security.py
src/pyams_content/shared/common/zmi/__init__.py
src/pyams_content/workflow/__init__.py
src/pyams_content/workflow/basic.py
--- a/src/pyams_content/shared/common/interfaces/__init__.py	Tue Oct 01 08:31:43 2019 +0200
+++ b/src/pyams_content/shared/common/interfaces/__init__.py	Tue Oct 01 12:04:20 2019 +0200
@@ -38,29 +38,33 @@
     """Shared tool roles interface"""
 
     webmasters = PrincipalsSet(title=_("Webmasters"),
-                               description=_("Webmasters can handle all contents, including published ones"),
+                               description=_("Webmasters can handle all contents, including "
+                                             "published ones"),
                                role_id=WEBMASTER_ROLE,
                                required=False)
 
     pilots = PrincipalsSet(title=_("Pilots"),
-                           description=_("Pilots can handle tool configuration, manage access rules, grant users "
-                                         "roles and manage managers restrictions"),
+                           description=_("Pilots can handle tool configuration, manage access "
+                                         "rules, grant users roles and manage managers "
+                                         "restrictions"),
                            role_id=PILOT_ROLE,
                            required=False)
 
     managers = PrincipalsSet(title=_("Managers"),
-                             description=_("Managers can handle main operations in tool's workflow, like publish "
-                                           "or retire contents"),
+                             description=_("Managers can handle main operations in tool's "
+                                           "workflow, like publish or retire contents"),
                              role_id=MANAGER_ROLE,
                              required=False)
 
     contributors = PrincipalsSet(title=_("Contributors"),
-                                 description=_("Contributors are users which are allowed to create new contents"),
+                                 description=_("Contributors are users which are allowed to "
+                                               "create new contents"),
                                  role_id=CONTRIBUTOR_ROLE,
                                  required=False)
 
     designers = PrincipalsSet(title=_("Designers"),
-                              description=_("Designers are users which are allowed to manage presentation templates"),
+                              description=_("Designers are users which are allowed to manage "
+                                            "presentation templates"),
                               role_id=DESIGNER_ROLE,
                               required=False)
 
@@ -83,11 +87,12 @@
 
     containers(ISharedToolContainer)
 
-    shared_content_menu = Attribute("Boolean flag indicating if tool is displayed into 'Shared contents' or "
-                                    "Shared tools' menu")
+    shared_content_menu = Attribute("Boolean flag indicating if tool is displayed into 'Shared "
+                                    "contents' or Shared tools' menu")
 
     shared_content_workflow = Choice(title=_("Workflow name"),
-                                     description=_("Name of workflow utility used to manage tool contents"),
+                                     description=_("Name of workflow utility used to manage tool "
+                                                   "contents"),
                                      vocabulary="PyAMS workflows",
                                      default="PyAMS default workflow")
 
@@ -119,12 +124,14 @@
     content_name = Attribute("Content name")
 
     content_url = TextLine(title=_("Content URL"),
-                           description=_("URL used to access this content; this is important for SEO and "
-                                         "should include most important words describing content; spaces and "
-                                         "underscores will be automatically replaced by hyphens"),
+                           description=_("URL used to access this content; this is important for "
+                                         "SEO and should include most important words describing "
+                                         "content; spaces and underscores will be automatically "
+                                         "replaced by hyphens"),
                            required=True)
 
-    handle_content_url = Attribute("Static boolean value to specify if content URL is supported by this content type")
+    handle_content_url = Attribute("Static boolean value to specify if content URL is supported by "
+                                   "this content type")
 
     creator = Principal(title=_("Version creator"),
                         description=_("Name of content's version creator. "
@@ -154,15 +161,19 @@
                            description=_("Content's header is generally displayed in page header"),
                            required=False)
 
-    handle_header = Attribute("Static boolean value to specify if header is supported by this content type")
+    handle_header = Attribute("Static boolean value to specify if header is supported by this "
+                              "content type")
 
     description = I18nTextField(title=_("Meta-description"),
-                                description=_("The content's description is 'hidden' into HTML's page headers; but it "
-                                              "can be seen, for example, in some search engines results as content's "
-                                              "description; if description is empty, content's header will be used."),
+                                description=_("The content's description is 'hidden' into HTML's "
+                                              "page headers; but it can be seen, for example, in "
+                                              "some search engines results as content's "
+                                              "description; if description is empty, content's "
+                                              "header will be used."),
                                 required=False)
 
-    handle_description = Attribute("Static boolean value to specify if description is supported by this content type")
+    handle_description = Attribute("Static boolean value to specify if description is supported by "
+                                   "this content type")
 
     keywords = TextLineListField(title=_("Keywords"),
                                  description=_("They will be included into HTML pages metadata"),
@@ -189,37 +200,39 @@
     """Shared content roles"""
 
     owner = PrincipalsSet(title=_("Content owner"),
-                          description=_("The owner is the creator of content's first version, except if it was "
-                                        "transferred afterwards to another owner"),
+                          description=_("The owner is the creator of content's first version, "
+                                        "except if it was transferred afterwards to another owner"),
                           role_id=OWNER_ROLE,
                           required=True,
                           max_length=1)
 
     managers = PrincipalsSet(title=_("Managers"),
-                             description=_("Managers can handle main operations in tool's workflow, like publish "
-                                           "or retire contents"),
+                             description=_("Managers can handle main operations in tool's "
+                                           "workflow, like publish or retire contents"),
                              role_id=MANAGER_ROLE,
                              required=False)
 
     contributors = PrincipalsSet(title=_("Contributors"),
-                                 description=_("Contributors are users which are allowed to update this content in "
-                                               "addition to it's owner"),
+                                 description=_("Contributors are users which are allowed to update "
+                                               "this content in addition to it's owner"),
                                  role_id=CONTRIBUTOR_ROLE,
                                  required=False)
 
     designers = PrincipalsSet(title=_("Designers"),
-                              description=_("Designers are users which are allowed to manage presentation templates"),
+                              description=_("Designers are users which are allowed to manage "
+                                            "presentation templates"),
                               role_id=DESIGNER_ROLE,
                               required=False)
 
     readers = PrincipalsSet(title=_("Readers"),
-                            description=_("Readers are users which are asked to verify and comment contents before "
-                                          "they are published"),
+                            description=_("Readers are users which are asked to verify and comment "
+                                          "contents before they are published"),
                             role_id=READER_ROLE,
                             required=False)
 
     guests = PrincipalsSet(title=_("Guests"),
-                           description=_("Guests are users which are allowed to view contents with restricted access"),
+                           description=_("Guests are users which are allowed to view contents "
+                                         "with restricted access"),
                            role_id=GUEST_ROLE,
                            required=False)
 
@@ -286,11 +299,19 @@
     """Shared content contributor restrictions"""
 
     publication_checks = Bool(title=_("Publication checks"),
-                              description=_("If 'yes', this contributor will have to confirm that contents have "
-                                            "been previewed and checked before asking for publication"),
+                              description=_("If 'yes', this contributor will have to confirm that "
+                                            "contents have been previewed and checked before "
+                                            "asking for publication"),
                               required=False,
                               default=True)
 
+    owners = PrincipalsSet(title=_("Substitute for"),
+                           description=_("Contributor will have access to contents owned by these "
+                                         "principals"))
+
+    def check_access(self, context, permission=MANAGE_CONTENT_PERMISSION, request=None):
+        """Check if principal is granted access to given context"""
+
 
 class IContributorRestrictions(IRestrictions):
     """Contributor restrictions interface"""
@@ -311,19 +332,21 @@
     """Shared content manager restrictions"""
 
     publication_checks = Bool(title=_("Publication checks"),
-                              description=_("If 'yes', this manager will have to confirm that contents have "
-                                            "been previewed and checked before publishing a content"),
+                              description=_("If 'yes', this manager will have to confirm that "
+                                            "contents have been previewed and checked before "
+                                            "publishing a content"),
                               required=False,
                               default=True)
 
     restricted_contents = Bool(title=_("Restricted contents"),
-                               description=_("If 'yes', this manager will get restricted access to manage contents "
-                                             "based on selected settings"),
+                               description=_("If 'yes', this manager will get restricted access "
+                                             "to manage contents based on selected settings"),
                                required=False,
                                default=True)
 
     owners = PrincipalsSet(title=_("Selected owners"),
-                           description=_("Manager will have access to contents owned by these principals"),
+                           description=_("Manager will have access to contents owned by these "
+                                         "principals"),
                            required=False)
 
     def check_access(self, context, permission=MANAGE_CONTENT_PERMISSION, request=None):
--- a/src/pyams_content/shared/common/security.py	Tue Oct 01 08:31:43 2019 +0200
+++ b/src/pyams_content/shared/common/security.py	Tue Oct 01 12:04:20 2019 +0200
@@ -10,19 +10,12 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION, MANAGER_ROLE, CONTRIBUTOR_ROLE
+from pyams_content.shared.common.interfaces import IWfSharedContent, IManagerRestrictions, \
+    MANAGER_RESTRICTIONS_KEY, IManagerRestrictionsFactory, IBaseSharedTool, IManagerRestrictionInfo, \
+    IRestrictionInfo, IContributorRestrictionInfo, IContributorRestrictions, \
+    IContributorRestrictionsFactory, CONTRIBUTOR_RESTRICTIONS_KEY, IRestrictions
 
-# import interfaces
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION, MANAGER_ROLE, CONTRIBUTOR_ROLE
-from pyams_content.shared.common.interfaces import IWfSharedContent, IManagerRestrictions, MANAGER_RESTRICTIONS_KEY, \
-    IManagerRestrictionsFactory, IBaseSharedTool, IManagerRestrictionInfo, IRestrictionInfo, \
-    IContributorRestrictionInfo, IContributorRestrictions, IContributorRestrictionsFactory, \
-    CONTRIBUTOR_RESTRICTIONS_KEY, IRestrictions
-
-# import packages
 from persistent import Persistent
 from pyams_security.interfaces import IPrincipalInfo, IRevokedRoleEvent, IGrantedRoleEvent
 from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter
@@ -34,6 +27,9 @@
 from zope.schema.fieldproperty import FieldProperty
 
 
+__docformat__ = 'restructuredtext'
+
+
 @implementer(IRestrictionInfo)
 class PrincipalRestrictionInfo(Persistent):
     """Principal restriction info"""
@@ -97,6 +93,16 @@
     restriction_interface = IContributorRestrictionInfo
 
     publication_checks = FieldProperty(IContributorRestrictionInfo['publication_checks'])
+    owners = FieldProperty(IContributorRestrictionInfo['owners'])
+
+    def check_access(self, context, permission=MANAGE_CONTENT_PERMISSION, request=None):
+        if request is None:
+            request = check_request()
+        if not request.has_permission(permission, context):  # check permission
+            return False
+        if context.owner & (self.owners or set()):  # check if owners are matching
+            return True
+        return False
 
 
 @adapter_config(context=IBaseSharedTool, provides=IContributorRestrictions)
--- a/src/pyams_content/shared/common/zmi/__init__.py	Tue Oct 01 08:31:43 2019 +0200
+++ b/src/pyams_content/shared/common/zmi/__init__.py	Tue Oct 01 12:04:20 2019 +0200
@@ -10,8 +10,6 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
 from datetime import datetime
 from uuid import uuid4
 
@@ -24,10 +22,10 @@
 from zope.location import locate
 
 from pyams_content.features.review.interfaces import IReviewComments
-from pyams_content.interfaces import CREATE_CONTENT_PERMISSION, MANAGE_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION, \
-    PUBLISH_CONTENT_PERMISSION
-from pyams_content.shared.common.interfaces import IBaseSharedTool, IManagerRestrictions, ISharedContent, \
-    IWfSharedContent
+from pyams_content.interfaces import CREATE_CONTENT_PERMISSION, MANAGE_CONTENT_PERMISSION, \
+    MANAGE_SITE_ROOT_PERMISSION, PUBLISH_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IBaseSharedTool, IContributorRestrictions, \
+    IManagerRestrictions, ISharedContent, IWfSharedContent
 from pyams_form.form import AJAXAddForm, ajax_config
 from pyams_form.interfaces.form import IFormContextPermissionChecker, IWidgetsPrefixViewletsManager
 from pyams_form.schema import CloseButton
@@ -44,20 +42,24 @@
 from pyams_skin.viewlet.breadcrumb import BreadcrumbAdminLayerItem
 from pyams_skin.viewlet.toolbar import ToolbarMenuDivider, ToolbarMenuItem
 from pyams_template.template import template_config
-from pyams_utils.adapter import ContextAdapter, ContextRequestAdapter, ContextRequestViewAdapter, adapter_config
+from pyams_utils.adapter import ContextAdapter, ContextRequestAdapter, ContextRequestViewAdapter, \
+    adapter_config
 from pyams_utils.interfaces import FORBIDDEN_PERMISSION
 from pyams_utils.registry import get_utility
 from pyams_utils.request import check_request
 from pyams_utils.traversing import get_parent
 from pyams_utils.url import absolute_url, generate_url
 from pyams_viewlet.viewlet import Viewlet, viewlet_config
-from pyams_workflow.interfaces import IWorkflow, IWorkflowCommentInfo, IWorkflowInfo, IWorkflowPublicationInfo, \
-    IWorkflowState, IWorkflowVersions
+from pyams_workflow.interfaces import IWorkflow, IWorkflowCommentInfo, IWorkflowInfo, \
+    IWorkflowPublicationInfo, IWorkflowState, IWorkflowVersions
 from pyams_workflow.versions import WorkflowHistoryItem
 from pyams_zmi.form import AdminDialogAddForm
 from pyams_zmi.interfaces.menu import ISiteManagementMenu
 from pyams_zmi.layer import IAdminLayer
 
+
+__docformat__ = 'restructuredtext'
+
 from pyams_content import _
 
 
@@ -108,7 +110,8 @@
         IWorkflowInfo(wf_content).fire_transition('init', comment=wf_content.notepad)
 
     def nextURL(self):
-        return absolute_url(self.context, self.request, '{0}/++versions++/1/admin#properties.html'.format(self.__uuid))
+        return absolute_url(self.context, self.request,
+                            '{0}/++versions++/1/admin#properties.html'.format(self.__uuid))
 
 
 class SharedContentAJAXAddForm(AJAXAddForm):
@@ -121,7 +124,8 @@
         }
 
 
-@viewlet_config(name='wf-create-message', context=Interface, layer=IPyAMSLayer, view=SharedContentAddForm,
+@viewlet_config(name='wf-create-message', context=Interface, layer=IPyAMSLayer,
+                view=SharedContentAddForm,
                 manager=IWidgetsPrefixViewletsManager, weight=20)
 @template_config(template='templates/wf-create-message.pt')
 class SharedContentAddFormMessage(Viewlet):
@@ -153,16 +157,20 @@
     def edit_permission(self):
         workflow = IWorkflow(self.context)
         state = IWorkflowState(self.context).state
-        if state in workflow.readonly_states:  # access forbidden to all for archived contents
+        if state in workflow.readonly_states:
+            # forbidden access to all for archived contents
             return FORBIDDEN_PERMISSION
-        elif state in workflow.protected_states:  # webmaster can update published contents
+        elif state in workflow.protected_states:
+            # webmaster can update published contents
             return MANAGE_SITE_ROOT_PERMISSION
         else:
             request = check_request()
-            if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, self.context):  # webmaster access
+            if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, self.context):
+                # webmaster access
                 return MANAGE_SITE_ROOT_PERMISSION
             principal_id = request.principal.id
-            if state in workflow.manager_states:  # restricted manager access
+            if state in workflow.manager_states:
+                # restricted manager access
                 if principal_id in self.context.managers:
                     return PUBLISH_CONTENT_PERMISSION
                 for parent in lineage(self.context):
@@ -177,8 +185,27 @@
                             else:
                                 return FORBIDDEN_PERMISSION
             else:
-                if principal_id in self.context.owner | self.context.contributors | self.context.managers:
+                # access is granted  to content's owner and designated contributors or managers
+                if principal_id in self.context.owner | \
+                                   self.context.contributors | \
+                                   self.context.managers:
                     return MANAGE_CONTENT_PERMISSION
+                # check if current principal can manage owner's contents
+                for parent in lineage(self.context):
+                    contrib_restrictions = IContributorRestrictions(parent, None)
+                    if contrib_restrictions is not None:
+                        user_restrictions = contrib_restrictions.get_restrictions(principal_id)
+                        if user_restrictions:
+                            if user_restrictions.check_access(self.context,
+                                                              permission=MANAGE_CONTENT_PERMISSION,
+                                                              request=request):
+                                return MANAGE_CONTENT_PERMISSION
+                restrictions = IContributorRestrictions(self.context).get_restrictions(principal_id)
+                if restrictions and restrictions.check_access(self.context,
+                                                              permission=MANAGE_CONTENT_PERMISSION,
+                                                              request=request):
+                    return MANAGE_CONTENT_PERMISSION
+                # check if current principal can manage content's due to manager restrictions
                 for parent in lineage(self.context):
                     manager_restrictions = IManagerRestrictions(parent, None)
                     if manager_restrictions is not None:
@@ -188,8 +215,6 @@
                                                               permission=MANAGE_CONTENT_PERMISSION,
                                                               request=request):
                                 return MANAGE_CONTENT_PERMISSION
-                            else:
-                                return FORBIDDEN_PERMISSION
                 restrictions = IManagerRestrictions(self.context).get_restrictions(principal_id)
                 if restrictions and restrictions.check_access(self.context,
                                                               permission=MANAGE_CONTENT_PERMISSION,
@@ -249,13 +274,15 @@
 #
 
 @viewlet_config(name='duplication.divider', context=IWfSharedContent, layer=IPyAMSLayer,
-                view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=49)
+                view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION,
+                weight=49)
 class WfSharedContentDuplicationMenuDivider(ToolbarMenuDivider):
     """Shared content duplication menu divider"""
 
 
 @viewlet_config(name='duplication.menu', context=IWfSharedContent, layer=IPyAMSLayer,
-                view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=50)
+                view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION,
+                weight=50)
 class WfSharedContentDuplicateMenu(ToolbarMenuItem):
     """Shared content duplication menu item"""
 
@@ -326,11 +353,13 @@
         state.history.clear()
         history = WorkflowHistoryItem(date=datetime.utcnow(),
                                       principal=self.request.principal.id,
-                                      comment=translate(_("Clone created from version {source} of {oid} "
-                                                          "(in « {state} » state)")).format(
+                                      comment=translate(
+                                          _("Clone created from version {source} of {oid} "
+                                            "(in « {state} » state)")).format(
                                           source=source_state.version_id,
                                           oid=ISequentialIdInfo(self.context).get_short_oid(),
-                                          state=translate(IWorkflow(self.context).get_state_label(source_state.state)))
+                                          state=translate(IWorkflow(self.context).get_state_label(
+                                              source_state.state)))
                                       )
         state.history.append(history)
         history = WorkflowHistoryItem(date=datetime.utcnow(),
@@ -371,5 +400,5 @@
                     translate = self.request.localizer.translate
                     value += '<i class="fa fa-fw fa-circle txt-color-orange hint align-middle padding-left-5" ' \
                              'title="{0}" data-ams-hint-gravity="w"></i>'.format(
-                                translate(_("Created or modified in this version")))
+                        translate(_("Created or modified in this version")))
         return value
--- a/src/pyams_content/workflow/__init__.py	Tue Oct 01 08:31:43 2019 +0200
+++ b/src/pyams_content/workflow/__init__.py	Tue Oct 01 12:04:20 2019 +0200
@@ -10,8 +10,6 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
 from datetime import datetime, timedelta
 
 from pyramid.threadlocal import get_current_registry
@@ -21,11 +19,12 @@
 from zope.location import locate
 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
 
-from pyams_content import _
-from pyams_content.interfaces import CREATE_VERSION_PERMISSION, MANAGE_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION, \
-    PUBLISH_CONTENT_PERMISSION
-from pyams_content.interfaces import MANAGER_ROLE, OWNER_ROLE, PILOT_ROLE, READER_ROLE, WEBMASTER_ROLE
-from pyams_content.shared.common.interfaces import IManagerRestrictions, IWfSharedContentRoles
+from pyams_content.interfaces import CREATE_VERSION_PERMISSION, MANAGE_CONTENT_PERMISSION, \
+    MANAGE_SITE_ROOT_PERMISSION, PUBLISH_CONTENT_PERMISSION
+from pyams_content.interfaces import MANAGER_ROLE, OWNER_ROLE, PILOT_ROLE, READER_ROLE, \
+    WEBMASTER_ROLE
+from pyams_content.shared.common.interfaces import IContributorRestrictions, IManagerRestrictions, \
+    IWfSharedContentRoles
 from pyams_content.workflow.interfaces import IContentWorkflow
 from pyams_content.workflow.task import ContentArchivingTask, ContentPublishingTask
 from pyams_scheduler.interfaces import IDateTaskScheduling, IScheduler
@@ -36,11 +35,17 @@
 from pyams_utils.registry import get_utility, query_utility, utility_config
 from pyams_utils.request import check_request
 from pyams_utils.timezone import gmtime
-from pyams_workflow.interfaces import AUTOMATIC, IWorkflow, IWorkflowInfo, IWorkflowPublicationInfo, IWorkflowState, \
-    IWorkflowStateLabel, IWorkflowVersions, ObjectClonedEvent, SYSTEM
+from pyams_workflow.interfaces import AUTOMATIC, IWorkflow, IWorkflowInfo, \
+    IWorkflowPublicationInfo, IWorkflowState, IWorkflowStateLabel, IWorkflowVersions, \
+    ObjectClonedEvent, SYSTEM
 from pyams_workflow.workflow import Transition, Workflow
 
 
+__docformat__ = 'restructuredtext'
+
+from pyams_content import _
+
+
 #
 # Workflow states
 #
@@ -137,12 +142,18 @@
     principal_id = request.principal.id
     if principal_id in context.owner | {context.creator} | context.contributors:
         return True
+    # grant access to allowed contributors
+    restrictions = IContributorRestrictions(context).get_restrictions(principal_id)
+    if restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                  request=request):
+        return True
     # grant access to local content managers
     if principal_id in context.managers:
         return True
     # grant access to shared tool managers if restrictions apply
     restrictions = IManagerRestrictions(context).get_restrictions(principal_id)
-    return restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION, request=request)
+    return restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                      request=request)
 
 
 def can_backdraft_content(wf, context):
@@ -173,6 +184,11 @@
     principal_id = request.principal.id
     if principal_id in context.owner | {context.creator} | context.contributors:
         return True
+    # grant access to allowed contributors
+    restrictions = IContributorRestrictions(context).get_restrictions(principal_id)
+    if restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                  request=request):
+        return True
     # grant access to local content managers
     if principal_id in context.managers:
         return True
@@ -191,6 +207,11 @@
     principal_id = request.principal.id
     if principal_id in context.owner | {context.creator} | context.contributors:
         return True
+    # grant access to allowed contributors
+    restrictions = IContributorRestrictions(context).get_restrictions(principal_id)
+    if restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                  request=request):
+        return True
     # grant access to local content managers
     if principal_id in context.managers:
         return True
@@ -227,6 +248,11 @@
     # owner, creator and contributors can cancel workflow request
     if principal_id in context.owner | {context.creator} | context.contributors:
         return True
+    # grant access to allowed contributors
+    restrictions = IContributorRestrictions(context).get_restrictions(principal_id)
+    if restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                  request=request):
+        return True
     # local content managers can cancel workflow request
     if principal_id in context.managers:
         return True
--- a/src/pyams_content/workflow/basic.py	Tue Oct 01 08:31:43 2019 +0200
+++ b/src/pyams_content/workflow/basic.py	Tue Oct 01 12:04:20 2019 +0200
@@ -10,8 +10,6 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
 from datetime import datetime, timedelta
 
 from zope.copy import copy
@@ -20,11 +18,10 @@
 from zope.location import locate
 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
 
-from pyams_content import _
 from pyams_content.interfaces import CREATE_VERSION_PERMISSION, MANAGER_ROLE, \
-    MANAGE_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION, OWNER_ROLE, PILOT_ROLE, PUBLISH_CONTENT_PERMISSION, \
-    READER_ROLE, WEBMASTER_ROLE
-from pyams_content.shared.common.interfaces import IManagerRestrictions
+    MANAGE_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION, OWNER_ROLE, PILOT_ROLE, \
+    PUBLISH_CONTENT_PERMISSION, READER_ROLE, WEBMASTER_ROLE
+from pyams_content.shared.common.interfaces import IContributorRestrictions, IManagerRestrictions
 from pyams_content.shared.common.interfaces import IWfSharedContentRoles
 from pyams_content.workflow import ContentArchivingTask, ContentPublishingTask
 from pyams_content.workflow.interfaces import IBasicWorkflow
@@ -36,11 +33,16 @@
 from pyams_utils.registry import get_current_registry, get_utility, query_utility, utility_config
 from pyams_utils.request import check_request
 from pyams_utils.timezone import gmtime
-from pyams_workflow.interfaces import IWorkflow, IWorkflowInfo, IWorkflowPublicationInfo, IWorkflowState, \
-    IWorkflowStateLabel, IWorkflowVersions, ObjectClonedEvent, SYSTEM
+from pyams_workflow.interfaces import IWorkflow, IWorkflowInfo, IWorkflowPublicationInfo, \
+    IWorkflowState, IWorkflowStateLabel, IWorkflowVersions, ObjectClonedEvent, SYSTEM
 from pyams_workflow.workflow import Transition, Workflow
 
 
+__docformat__ = 'restructuredtext'
+
+from pyams_content import _
+
+
 DRAFT = 'draft'
 PRE_PUBLISHED = 'pre-published'
 PUBLISHED = 'published'
@@ -127,6 +129,11 @@
     principal_id = request.principal.id
     if principal_id in context.owner | {context.creator} | context.contributors:
         return True
+    # grant access to allowed contributors
+    restrictions = IContributorRestrictions(context).get_restrictions(principal_id)
+    if restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                  request=request):
+        return True
     # grant access to local content managers
     if principal_id in context.managers:
         return True
@@ -145,6 +152,11 @@
     principal_id = request.principal.id
     if principal_id in context.owner | {context.creator} | context.contributors:
         return True
+    # grant access to allowed contributors
+    restrictions = IContributorRestrictions(context).get_restrictions(principal_id)
+    if restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION,
+                                                  request=request):
+        return True
     # grant access to local content managers
     if principal_id in context.managers:
         return True