Permissions

Description

How to deal with permissions making your code permission aware in Plone

Introduction

Permissions control whether the logged in / anonymous users can execute code paths.

Permission check is done for

  • For every view/method which is hit by incoming HTTP request (Plone automatically exports traversable methods over HTTP interface)
  • For every called method for RestrictedPython scripts

The basic way of dealing with permissions is setting the permission attribute of view declaration. For more information see views.

Checking if the logged in user has a permission

The following code checks whether the logged in user has a certain permission for the some object.

from AccessControl import getSecurityManager
from AccessControl import Unauthorized

# Import permission names as pseudo-constants strings from somewhere... see security doc for more info
from Products.CMFCore import permissions

def some_function(self, object):


    # This will
    if not getSecurityManager().checkPermission(permissions.ModifyPortalContent, object):
        raise Unauthorized("You need ModifyPortalContent permission to edit header animations")

     # ...
     # we have security clearance here
     #

Checking if a specific role has a permission

Example which checks if Authenticated role has a permission on a certain folder on the site using rolesOfPermission():

def checkDBPermission(self):

        from zope.app.component.hooks import getSite

        site = getSite()

        obj = site.intranet

        perms = obj.rolesOfPermission("View")

        found = False

        for perm in perms:
            if perm["name"] == "Authenticated":
                if perm["selected"] != "": # will be SELECTED if the permission is granted
                    found = True
                    break

        if not found:
            from Products.statusmessages.interfaces import IStatusMessage
            messages = IStatusMessage(self.request)
            messages.addStatusMessage(u"Possibe permission access problem with the intranet. Errors on creation form may happen.", type="info")

Permission Access

Object that are manageable ttw inherit of RoleManager. API class provided by this class permit you to manage permission.

Example :

  • see all possibles permissions
>>> object.possible_permissions()
['ATContentTypes Topic: Add ATBooleanCriterion', 'ATContentTypes Topic: Add ATCurrentAuthorCriterion',...
  • Show the security matrix of permission
 >>> self.portal.rolesOfPermission('Modify portal content')
[{'selected': '', 'name': 'Anonymous'}, {'selected': '', 'name': 'Authenticated'}, {'selected': '', 'name': 'Contributor'}, {'selected': '', 'name': 'Editor'}, {'selected': 'SELECTED', 'name': 'GroupAdmin'}, {'selected': '', 'name': 'GroupContributor'}, {'selected': '', 'name': 'GroupEditor'}, {'selected': '', 'name': 'GroupLeader'}, {'selected': '', 'name': 'GroupMember'}, {'selected': '', 'name': 'GroupReader'}, {'selected': '', 'name': 'GroupVisitor'}, {'selected': 'SELECTED', 'name': 'Manager'}, {'selected': '', 'name': 'Member'}, {'selected': 'SELECTED', 'name': 'Owner'}, {'selected': '', 'name': 'Reader'}, {'selected': '', 'name': 'Reviewer'}, {'selected': '', 'name': 'SubscriptionViewer'}]

Bypassing permission checks

The current user is defined by active security manager. In both restricted and unrestricted execution certain functions may do their own security checks (invokeFactory, workflow, search) to filter out results.

If function does its own security check, there usually a code path to execute without security check. For example the methods below have security-aware and raw versions

  • context.restrictedTraverse() vs. context.unrestrictedTraverse()
  • portal_catalog.searchResults() vs. portal_catalog.unrestrictedSearchResults()

However, in certain situations you have only security-aware code path which is blocked for the current user. You still want to execute this code path and you are sure that it does not violate your site security principles.

Below is an example how you can call any Python function and work around the security checks by establishing a temporary AccessControl.SecurityManager under special role.

Example:

from AccessControl import ClassSecurityInfo, getSecurityManager
from AccessControl.SecurityManagement import newSecurityManager, setSecurityManager
from AccessControl.User import nobody
from AccessControl.User import UnrestrictedUser as BaseUnrestrictedUser

class UnrestrictedUser(BaseUnrestrictedUser):
    """Unrestricted user that still has an id.
    """
    def getId(self):
        """Return the ID of the user.
        """
        return self.getUserName()

def execute_under_special_role(portal, role, function, *args, **kwargs):
    """ Execute code under special role priviledges.

    Example how to call::

        execute_under_special_role(portal, "Manager",
            doSomeNormallyNotAllowedStuff,
            source_folder, target_folder)


    @param portal: Reference to ISiteRoot object whose access controls we are using

    @param function: Method to be called with special priviledges

    @param role: User role we are using for the security context when calling the priviledged code. For example, use "Manager".

    @param args: Passed to the function

    @param kwargs: Passed to the function
    """

    sm = getSecurityManager()

    try:
        try:

            # Clone the current access control user and assign a new role for him/her
            # Note that the username (getId()) is left in exception tracebacks in error_log
            # so it is important thing to store
            tmp_user = UnrestrictedUser(
              sm.getUser().getId(),
               '', [role],
               ''
           )

            # Act as user of the portal
            tmp_user = tmp_user.__of__(portal.acl_users)
            newSecurityManager(None, tmp_user)

            # Call the function
            return function(*args, **kwargs)

        except:
            # If special exception handlers are needed, run them here
            raise
    finally:
        # Restore the old security manager
        setSecurityManager(sm)

For more information, see

Catching Unauthorized

Gracefully failing when the user does not have a permission. Example:

from AccessControl import Unauthorized

try:
    portal_state = context.restrictedTraverse("@@plone_portal_state")
except Unauthorized:
    # portal_state may be limited to admin users only
    portal_state = None

Assigning permissions to users (roles)

Permissions are usually assigned to roles, which are assigned to users through the web.

To assign a permission to a role, use profiles/default/rolemap.xml:

<?xml version="1.0"?>
 <rolemap>
   <permissions>
     <permission name="MyProduct: MyPermission" acquire="False">
       <role name="Member"/>
     </permission>
   </permissions>
 </rolemap>

Manually fix permission problems

In the case you fiddle with permission and manage lock out even admin user for the content (both Plone page and raw Zope page) you can still fix the problem from debug prompt.

Example debug session how to set Access Contents Information back to all users:

>>> j=app.yoursiteid.yourfolderid.problematiccontent
>>> import AccessControl
>>> import Products.CMFCore.permissions
>>> sm=AccessControl.getSecurityManager()
>>> import transaction
>>> anon=sm.getUser()
>>> j.manage_permission(Products.CMFCore.permissions.AccessContentsInformation,roles=anon.getRoles())
>>> transaction.commit()

Creating permissions

Define both Zope permissions in one Step in ZCML

You can use collective.autopermission <http://pypi.python.org/pypi/collective.autopermission/1.0b1> (svn repository <http://svn.plone.org/svn/collective/collective.autopermission>) and define both the Zope 2 and Zope 3 permission at once with the <permission> zcml-directive. To do that install collective.autopermission. Either add "collective.autopermission" to "install_requires" in setup.py or to your buildout. Then include collective.autopermission's configure.zcml before you define the permissions and before you use them. (collective.autopermission is not required in Zope 2.12/Plone 4 anymore!)

<configure
  xmlns="http://namespaces.zope.org/zope"
  xmlns:browser="http://namespaces.zope.org/browser">

  <include package="collective.autopermission" />

  <permission
    id="myproduct.mypermission"
    title="MyProduct: MyPermission"
    />

  <browser:page
    for="*"
    name="myexampleview"
    class="browser.MyExampleView"
    permission="myproduct.mypermission"
    />

</configure>

Now you can use the permission both as a Zope 2 permission ('MyProduct: MyPermission') or a Zope 3 permission ('myproduct.mypermission'). The only disadvantage is that you can't import the permissionstring as a variable from permissions.py.

Define Zope 2 permissions in Python code (old style)

If you want to protect certain actions in your product by a special permission, you most likely will want to assign this permission to a role when the product is installed. You will want to use Generic Setup's rolemap.xml to assign these permissions. A new permission will be added to the Zope instance by calling setDefaultRoles on it.

However, at the time when Generic Setup is run, almost none of your code has actually been run, so the permission doesn't exist yet. That's why we define the permissions in permissions.py, and call this from __init__.py:

# __init__.py:

import permissions
# permissions.py:

from Products.CMFCore import permissions as CMFCorePermissions
from AccessControl.SecurityInfo import ModuleSecurityInfo
from Products.CMFCore.permissions import setDefaultRoles

security = ModuleSecurityInfo('MyProduct')
security.declarePublic('MyPermission')
MyPermission = 'MyProduct: MyPermission'
setDefaultRoles(MyPermission, ())

When working with permissions, always use the variable name instead of the string value. This ensures that you can't make typos with the string value, which are hard to debug. If you do make a typo in the variable name, you'll get an ImportError or NameError.

Making permission available as Zope 3 permission

To use your permissions with Zope 3 technologies e.g. BrowserViews/formlib/z3c.form, you need to make them available available as Zope 3 permissions. This is done in ZCML using a the <permission> directive. Example configure.zcml:

<configure
  xmlns="http://namespaces.zope.org/zope">

  <permission
    id="myproduct.mypermission"
    title="MyProduct: MyPermission"
    />

</configure>

It's convention to prefix the permission id with the nmame of the package it's defined in and use lower case only. You have to take care that the title matches exactly the permission string you used in permissions.py. Otherwise a different, zope 3 only, permission is registered.

You can use the permission to e.g. protect BrowserViews. Example configure.zcml:

<configure
  xmlns="http://namespaces.zope.org/zope"
  xmlns:browser="http://namespaces.zope.org/browser">

  <permission
    id="myproduct.mypermission"
    title="MyProduct: MyPermission" />

  <browser:page
    for="*"
    name="myexampleview"
    class="browser.MyExampleView"
    permission="myproduct.mypermission"
    />

</configure>



Edit this document

The source code of this file is hosted on GitHub. Everyone can update and fix errors in this document with few clicks - no downloads needed.

  1. Go to Permissions on GitHub.
  2. Press Fork and edit this file button.
  3. Edit file contents using GitHub's text editor in your web browserm
  4. Fill in the Commit message text box at the end of the page telling why you did the changes. Press Propose file change button next to it when done.
  5. On Send a pull request page you don't need to fill in text anymore. Just press Send pull request button.
  6. Your changes are now queued for review under project's Pull requests tab on Github.

For basic information about updating this manual and Sphinx format please see Writing and updating the manual guide.