Skip to content.

Personal tools
You are here: Home » Members » chrism's Home » Decsec Proposal

Decsec Proposal

A proposal for a standalone Python declarative security implementation for use as a component in web applications.

Core Concepts

The Decsec declarative security machinery computes an access control list based on the traversal of a path.

Decsec is meant to be used as a component in a processing chain such as WSGI, although it could be used for any application.

Decsec refrains from enforcing a security policy. It only injects enough information into "the processing chain" to make it possible for your application to enforce its own policy. You may ignore the information provided by Decsec entirely within your application if you so choose.

You are meant to make use of Decsec by protecting resources (which are assumed to live at paths) with permissions.

Decsec provides APIs to check and manipulate ACLs within your application.



A path is a forward-slash-separated string which represents a context within your application. Often, this may just be the path implied by the URL of a web request.


A user is an object representing a person.


A group is an object representing zero or more other users or groups.


A principal is an abstract notion of a user or group. In various places within Decsec, a user or group can be used interchangeably, so it's useful to canonize this abstraction.


Permissions are strings that represent actions which can be performed in the context of your application.

Permission Group (aka Pgroup)

A permission group is a set of of zero or more permissions or permission groups. A permission group is to a permission as a group is to a user.

Access Control Lists (ACLs)

An access control list is an ordered list of rules that are meant to be processed to enforce security. Each rule specifies at least three things: a principal, a permission or pgroup, whether that permission or pgroup is allowed or denied. A rule may also include an optional condition. A rule is also known as an ACE, or "access control entry".

Access Control Lists In Detail

A simple example of an access control list in natural language is as follows:

  1. Allow all users to read.
  2. Allow authenticated users to write.
  3. Deny access to all users.

In the above example, there are three "rules" or ACEs (access control entries). As hinted at by the numbers before them, their relative order is meaningful.

The first rule "allow all users to read" has an action ("allow"), a principal ("all users") and a permission ("read"). There are only two actions that are meaningful to Decsec: "allow" and "deny". These are self-explanatory. The principal "all users" is expressed in Decsec using a principal id (a user or group identifier). In this case, "all users" can be expressed in Decsec using the special Decsec group "system.Everyone". The permission "read" in terms of Decsec implies a permission or pgroup identifier. For the purposes of demonstration, we will use the pgroup identifer "application.Read" to represent a set of permissions.

So the first rule "allow all users to read" when translated into a Decsec ACE can be expressed in terms of the Python dictionary:

    {'action':'allow', 'principal':'system.Everyone',

We also choose to represent the predicate "write" with the pgroup identifier "application.Write" and the subject "authenticated users" with the special Decsec group "system.Authenticated".

The ACL that represents the above ruleset therefore can be expressed in the terms of this Python list:

   [   {'action':'allow', 'principal':'system.Everyone',
       {'action':'allow', 'principal':'system.Authenticated',
        'permission':'application.Write'}  ]

The last rule in the above example ("deny access to all users") is implied in every ACL, so it needn't be explicitly supplied. This is the default rule that will be guaranteed to deny access during encforcement if none of the other rules in an ACL generate an allow or deny. This follows the general security rule "everything which is not explicitly allowed is denied".

Another simple example of an access control list is as follows:

  1. Allow Bob to write.
  2. Allow Ray to read.
  3. Allow the group "Staff" to delete
  4. Deny access to all users.

This might be expressed in Decsec terms as:

     [ {'action':'allow', 'principal':'Bob',   'permission':'system.Write'},
       {'action':'allow', 'principal':'Ray',   'permission':'system.Read'},
       {'action':'deny',  'principal':'Staff', 'permission':'system.Delete'} ]

A more complicated example might include a condition. For example, the ruleset below is a bit more complicated:

  1. Allow Bob to write if he's using a remote machine with the IP address "".
  2. Allow Ray to read.
  3. Allow the group "Staff" to delete
  4. Deny access to all users.

In Decsec this becomes the Python list:

     [ {'action':'allow', 'principal':'Bob',   'permission':'system.Write'
        'condition': 'python: path("request/REMOTE_ADDR") == ""'},
       {'action':'allow', 'principal':'Ray',   'permission':'system.Read'},
       {'action':'deny',  'principal':'Staff', 'permission':'system.Delete'} ]

Conditions are written as TALES (Template Attribute Language Expression Syntax) expressions within Decsec. These expressions are not evaluated by Decsec during traversal or ACL creation, they are only evaluated when an the ACL is processed by your application. The available top-level bindings within Decsec TALES condition expressions are application-defined.


Decsec resolves a path to an ACL in the following way:

  • It splits the path on forward slashes, creating a list of path elements.
  • Beginning at the root of the "path space", Decsec begins to traverse through nodes in the path space while iterating through the path elements. It stops iterating when it finds a path element that it cannot resolve.

  • If the node upon which traversal stops has an ACL attached, this becomes the ACL that is passed into the remainder of the processing chain.
  • If the node upon which it stops does not have an ACL attached to it for some reason, Decsec travels "up" the path until it finds an ACL and that ACL is passed into the remainder of the processing chain.

The root of the Decsec path space always has an ACL, so if a path cannot be resolved at all, the root ACL is used implicitly.

Note that an ACL is not composed dynamically during traversal. One and only one static ACL will be found at the end of traversal.

ACL Objects

Although an ACL is really just declarative data, the data must be processed by your application for it to be useful. For this purpose, an ACL in Decsec is represented as an object with an API that is supplied to your Python application. This object is passed into the application through the processing chain (perhaps in the request object for a web application).

An ACL object has one method named "check". The "check" method return value is a boolean. The "check" method accepts two positional arguments: "princid" and "permission" and a single optional keyword argument "expr_context". Its method signature is shown below and its intent is indicated in the docstring:

    def check(princid, permission, expr_context=None):

        """ This method returns True if an ACE within this ACL allows
        the user or group implied by 'princid' to carry out an
        operation implied by the permission or pgroup implied by
        'permission'.  This method returns False if the this ACL
        denies the operation implicitly by finding no allow match or
        explicitly via an ACE 'deny' action.  The optional
        'expr_context' argument is used to resolve Decsec condition
        expressions.  If it is not supplied, ACEs in this ACL which
        possess a condition expression are skipped. """

The princid argument is the meant to be the identifier of the user who is currently making a request. This user's group memberships will be accounted for during processing of Decsec ACEs that mention groups. The permission argument is meant to be the distinct permission which protects the content resource (or "view") that matches the request path. Pgroups will be "flattened" into distinct permissions during Decsec ACL processing, so if an ACL mentions a pgroup that contains this permission, it will be accounted for in the result.

If you do supply an expression context via expr_context, ACEs with conditions will be processed. Errors raised during a condition expression evaluation on an ACE will cause the ACE to deny.

Associating ACLs with Paths via a GUI

Decsec provides a GUI application to manage the creation of nodes and the association of nodes with ACLs. When you visit a node in the GUI, you can choose to:

  • Change the ACL associated with this node.
  • Add a new subnode.
  • Delete subnodes.

If you change the ACL of a node which itself has subnodes, the system will prompt you to choose whether to apply the ACL to all subnodes. If you choose yes, you can apply the ACL to all subnodes in merge or overwrite mode. Choosing overwrite will effectively delete all subnodes. Choosing merge will use a best-effort algorithm to merge the rules in the changed ACL to all subnodes with their own distinct ACLs.

Associating ACLs with Paths Programmatically

Your application may programmatically add/remove nodes from Decsec or change an ACL associated with a node and any other features implied by the GUI application.

Decsec can operate in a transactional mode to support systems which need to change data transactionally. XXX This needs to be fleshed out.

A "" object will be added to the processing chain that represents the node tree. It supports the following methods:

     def addNode(self, path):
         """ Adds a node at path """

     def delNode(self, path):
         """ Deletes the node (and all subnodes) represented at 'path' """

     def changeNode(self, path, acl, cascade=None):
         """ Changes the ACL data at 'path' using the acl data implied
         by 'acl'.  If 'cascade' is 'overwrite', delete all subnodes
         underneath 'path' in the map. If 'cascade' is 'merge', merge
         the ACL supplied into all subnodes that possess their own
         ACLs """

User Sources

Decsec will obtain its user data from arbitrarily-defined user sources (such as LDAP, UNIX, SQL, etc). Zope's PAS will be used as a model.

Group Sources

Decsec will obtain its user data from arbitrarily-defined group sources (such as LDAP, UNIX, SQL, etc). Zope's PAS will be used as a model.

Permission Sources

Your applications define the available permission identifiers. You can inject these permission into Decsec programmatically as necessary to make it easier for administrators to define security policies.

Permission Group Sources

Your application may group permissions into pgroups and inject these pgroups into Decsec programmatically. An administrator may create new pgroups which group other pgroups and permissions arbitrarily.

Auditing and Debugging

When a Decsec ACL object's "check" method is called, a log entry is written describing the processing result. The log entry contains the path at which the ACL was found in the node map, a description of the last ACE processed, and the final result of the processing (allowed or denied). If an error occurs during condition expression evaluation, the error traceback will also be included.

Decsec Contrasted Against the Zope Security Machinery

Zope's internal security machinery associates a security policy with an object. Decsec associates a security policy with a path. If an object moves from one place to another within Zope (thereby effecting its path) and this object has its own local security settings, those settings come along with it into the new place. In Decsec, they do not. If this behavior is desired in your application, the object's settings must be moved separately using a Decsec API call in terms of a path. Likewise, if an object with security settings is removed, you must remove the Decsec data for the path represented by the object from within your application. This can likely be automated through the use of a content-aware event system.

Zope's security machinery enforces its security policy during and/or after object graph traversal. Decsec does not enforce a security policy without help from your application.

Decsec allows you to associate access control lists with paths. Zope (2, at least) does not use access control lists.

Created by chrism
Last modified 2005-07-15 01:57 PM

Random nits

1. "A role is a collection of permissions"

If you mean this to make sense to either security folks or Zope folks, that isn't quite precise;
a role is actually an "abstract grant" of one or more permissions. The "permission group" itself
is a separate notion. Note that some applications in Zopeland use Zope's roles to fake out
*user groups*; others use them as you indicate.

2. "The first rule ... has an action ('allow'), a subject ('all users'), and a predicate ('read')"

In RDF-speak, the predicate would be "is allowed" and the object would be "read". Isn't the
"object" really just a permission? WHy don't we just call it that?

3. "The last rule ... ("denay access to all users") is implied"

Why? Why not make it explicit?

4. I imagine that many applications are going to want to have ACLs "inherited" and "overridden"
(but then you are basically back to Zope's model).

5. "Zope's internal security machinery controls access to objects."

True for Zope2; Zope3 protects "names", not objects, and does so at each
'getattr' (via security proxies).

5. "Zope's security machinery enforces its security policy during traversal."

For clarity: in Zope2, this statement is true, *except* during "publishing" traversal (which is
what you mostly care about); there, the publishing traversal is done *without* access checks,
(but roles are collected), and the published object is tested (this is the "inside out" policy
which makes ChrisW foam at the mouth).

Throwing fuel on the fire

Let's take my frustrations with Zope declarative security and throw some fuel on this fire.

1) The Zope security model doesn't have any layers. By this I mean a notion where there is a cascading set of rules, whereupon the system doesn't have one and only one place to look to grant rights. For example: the system default policy, the group default policy, the entity default policy, the object policy, the entity override policy, the group override policy, the system override policy. So, I might in my system default policy say "deny * to all" but in my group default policy say "grant * to all" (where ALL is all members of my group)... but then in my system override policy I could say "deny * to blocked_user" to lock out a specific user. In a large system, the end user may be able to only set certain policies, which can be supplemented by the default rules and overriden by a override policies.

2) The Zope model doesn't have any way to provide for computed roles. Imagine a simple system with a hierarchial set of folders and simple roles. To provide n * m role assignment (ie, user K is an Author in the Foo folder) one has to visit every single stupid folder and set local roles on it. If there are lots of users, you can use GRUF in Plone and assign a group to the role to consolidate permission setting to one spot, but you still end up saying local role Author = group_foo_authors on the Foo folder. There is no facility to evaluate an expression to determine the permission (ie, grant update where user_groups contains "author" and user_groups contains "foo")

This latter part tends to be frustrating when you get the "oh, the Zope security model is very flexible" tossed back in your teeth. True, its flexible, but it isnt easily ADMINISTERED in this instance without writing custom components or a custom security policy. Neither is particularly attractive.

However it gets implemented... this is a general Python problem, not just Zope.

For greatest success, this should be a library that can be used by any Python application outside of Zope. For example, I would like to use an ACL library for CherryPy or in Django... And it would be a shame to have to rewrite one for every framework... The true test of DecSec is to show working examples for more than one framework besides Zope. Basically, I'm looking for a general purpose Python version of phpgacl (