Package: policies
A package for interpretation and enforcement of access control
policies.IntroductionIt is often necessary to separate code that performs an action from
the code that performs the access check.  One reason for this is to
accommodate different users with different access control
requirements.  For instance, one user may be operating a system
internally, where all authenticated users should be able to perform
all actions, whereas another user may need to lock down specific
operations so they can only be executed by administrators.Thepoliciespackage is designed to accommodate these needs.
Access control policies can be expressed as strings, using a subset of
Python; then, these policies can be loaded into apolicies.Policyobject.  When an access determination needs to be made, a call to thepolicies.Policy.evaluate()method will evaluate a named policy
rule and return anAuthorizationobject, which evaluates as eitherTrueorFalse.The policy strings may be loaded from any source.  They are simply
strings, written in a subset of the Python language, and allow much of
the expressive power of Python.  The policy language has syntax for
making function calls, including functions defined asentrypoints;
this allows any desired access control policy to be implemented for
any application usingpolicies.policiesfor DevelopersThepoliciespackage is easy for developers to use; simply
instantiate apolicies.Policyobject with an optional entrypoint
group and dictionary of built-in functions (defaults to select Python
builtins, available aspolicies.Policy.builtins), then add rules
to the object.  This can be done by assigning the rule text using the
dictionary item setting syntax, like so:policy['rule_name'] = "user.is_admin()"Alternatively, the rule text can be passed topolicies.Ruleand
set using thepolicies.Policy.set_rule()method, like so:rule = policies.Rule("rule_name", "user.is_admin()")
policy.set_rule(rule)These two different methods allow for the rules to be loaded from any
desired source, such as a file or a database.Evaluation of a policy rule is as simple as calling thepolicies.Policy.evaluate()function:authz = policy.evaluate("rule_name", {'user': user})Theauthzvalue can then be used to determine if the operation is
allowed by the policy:if authz:
    # Perform the operation here
    pass
else:
    # Tell the user he's unprivileged
    passNote the dictionary passed as the second argument topolicy.evaluate()above; this allows variables to be passed in to
policy rules.Authorization AttributesThe return value frompolicy.evaluate()is not a simpleTrueorFalsevalue; it is an instance ofpolicies.Authorization.
The reason for this is that the policy language allows for settingauthorization attributes.  To explain what this is about, let’s
assume that the operation we’re writing a policy for is a user update
operation.  Obviously, we want the user to be able to update certain
parts of their own record, but others–say, payment status–should
only be available to administrators.  We can write this all in one
rule in the policy language:user.is_admin() or user == target {{ payment=user.is_admin() }}When we evaluate this rule, thepolicies.Authorizationobject
returned will testTrueorFalsedepending on the result of
evaluating the first part of the rule,user.is_admin() or user == target.  However, theauthzobject will now also have an
attribute namedpayment; this attribute will have the value
obtained by computinguser.is_admin().Authorization attributes default toNoneif the policy language
doesn’t set them.  This default can be overridden by passing a
dictionary of attribute defaults to thepolicies.Ruleinstance
when it is created, or by declaring the rule usingpolicies.Policy.declare().Note that authorization attribute names CANNOT begin with an
underscore (“_”).Declaring Policy RulesSetting policy rules has been described above, but what about setting
up defaults for the policy rules?  This can be done using thepolicies.Policy.declare()method:policy.declare("rule_name", text="user.is_admin()")This can also be used to set defaults for authorization attributes, by
passing a dictionary of those defaults as theattrskeyword
argument.Thepolicy.declare()method also allows associating documentation
text with the rule and the authorization attributes, using thedocandattr_docskeyword arguments; callingpolicy.declare()will
result in the creation ofpolicies.RuleDocobjects to contain the
passed-in documentation.  These objects can be retrieved using thepolicies.Policy.get_doc()andpolicies.Policy.get_docs()methods, and could be used to generate sample policy configuration
files.Variable Resolution in Policy RulesWhen a variable is encountered in a policy rule, it must be resolved
to an actual value.  The first place searched when resolving variables
is the dictionary of variables that was passed topolicies.Policy.evaluate(); values passed here override any other
source.If the variable cannot be found in the dictionary passed topolicies.Policy.evaluate(), then a dictionary of builtins is
searched; by default, these builtins are the ones inpolicies.Policy.builtins, and represent a subset of the Python
builtins.  These builtins can be overridden by passing a dictionary as
thebuiltinsparameter of thepolicies.Policyconstructor.
Note that one special builtin exists which is not listed inpolicies.Policy.builtins, and which will be added to the builtins
passed to thepolicies.Policyconstructor: therule()builtin
allows for one rule to call another.  It can be overridden, if
desired, by passing an alternate value for the “rule” key in thebuiltinsdictionary.If the variable cannot be resolved from either of the sources above,
it is next searched for usingentrypoints.  The entrypoint group to
search can be specified as thegroupargument to thepolicies.Policyconstructor.  There is no default for the
entrypoint group, so if left unset, no entrypoints will be resolved.
Any entrypoints found will be cached for the lifetime of thepolicies.Policyobject.  It is recommended that you setgroupto be the name of your application, followed by a period, followed by
the name “policies”; e.g., if your application was called “spam”, you
would use “spam.policies”.  Using an entrypoint group allows your
users to set up arbitrary functions for use in evaluating access
control policies, and thus allows them ultimate control over access.If a variable cannot be resolved using any of the above sources, its
value will beNone.  This is as opposed to the standard Python
behavior of raising aNameError.  Thepoliciespackage is
designed to be as tolerant of user errors as possible.policiesfor UsersPolicy rules are written in a subset of the Python expression
language.  The singleton valuesTrue,False, andNoneare
recognized, as are single- and double-quoted strings, integers, and
floats.  The set literal syntax is also recognized, i.e.,{1, 2, 3}represents the valuefrozenset([1,2, 3]).  Tuple literals,
list literals, dictionary literals, and comprehensions are not
supported, although thetuple(),list(), anddict()builtins are available, as areset()andfrozenset().In addition to the literal values mentioned above, the policy language
also supports attribute reference, subscription (x[index]), and
function calls.  Note that “slicing” (x[index:index]) is not
supported, however.  Finally, all arithmetic, logical, and comparison
operators are supported, as is the Python “trinary” syntax (a if b else c).As an example, let’s suppose that a particular rule is controlling
update access to a user record.  Theuservariable will be the
user requesting the operation, andtargetwill be the user record
the operation is to act upon.  The policy we want to implement is to
allow a given user to update only their own record, but we want
administrators to be able to update any user record.  We’ll assume
thatuserhas a boolean attribute namedadminthat isTrueif the user is an administrator.  Under these assumptions, the policy
rule could be written as:user == target or user.adminIt is also possible to call methods on an object.  Lets say that,
instead of a boolean attribute namedadminthat specifies whether
a user is an admin, we instead base administrator status on the
members of a group.  We assume that theuserobject has anin_group()method.  We could then write the rule as:user == target or user.in_group("administrators")Finally, it is also possible to call functions.  If thepolicies.Policy()class was instantiated with an entrypoint group,
you can install a package with a function defined in that entrypoint
group (seeentrypoints), which will then be available to policy
rules.  This allows ultimate control over access control.  Note that
only positional arguments can be passed to functions; keyword
arguments are not available.Note that operator short-circuiting is implemented; that is, in an
expression likeuser == target or user.admin, if theuser == targetclause evaluates toTrue, thenuser.adminwill not be
evaluated.  This applies for the logical operators (andandor), as well as in the “trinary” syntax.  Constant folding is also
implemented, so rule text like5 + 23 > user.spamwill only
compute the operation5 + 23once, during rule parsing.Authorization AttributesLet us take the example from above and add one more requirement.
Let’s say that one of the things the user update operation can update
is the current payment status on a user.  Obviously, that is something
that we don’t want a user to be able to update; only administrators
should be able to update the payment status.  A developer can allow
this particular subset of functionality to be controlled separately
using anauthorization attribute.  For the example above, let’s
assume that thepaymentauthorization attribute can control access
to the update of the payment status.  Now we can rewrite the policy
rule as:user == target or user.admin {{ payment=user.admin }}More than one authorization attribute can be computed by separating
them with commas.  Let’s assume that we have an authorization
attributenamethat allows updating the user’s name, and we want
to allow only the user to alter the name; we could write the rule as:user == target or user.admin {{ payment=user.admin,
                                name=user==target }}Evaluating Other RulesEach rule has an associated name.  It is possible to define an
arbitrary rule, and then evaluate it from another rule.  Taking our
example from above, let’s assume that an admin must not only be in the
“administrators” group, but must also haveadminset toTrueon their user record.  (This could be the case if your policy requires
administrators to explicitly turn on their administrative privileges.)
We could create an “is_admin” rule that looks like this:user.in_group("administrators") and user.adminWe could then write the rule controlling access to the user update
operation as:user == target or rule("is_admin")Note that any authorization attributes on the “is_admin” rule will be
ignored; to set an authorization attribute on the user update
operation, they have to be explicitly declared:user == target or rule("is_admin") {{ payment=rule("is_admin"),
                                      name=user==target }}Available BuiltinsThe following Python builtins are available:abs()basestring()bin()bool()bytes()callable()chr()complex()dict()divmod()enumerate()float()format()frozenset()getattr()hasattr()hash()hex()id()int()isinstance()issubclass()iter()len()list()long()max()min()next()object()oct()ord()pow()range()repr()reversed()round()set()sorted()str()sum()tuple()type()unichr()unicode()xrange()zip()Advanced Function CallsUnder normal circumstances, functions are called with only the
arguments passed in the rule text, and their return values are then
pushed onto the stack in place of those function arguments.  However,
certain functions–such as therule()function–need access to the
context object (policies.PolicyContext).  In the case ofrule(), this allows it to keep a cache of rules that have been
evaluated for the duration of thepolicies.Policy.evaluate()call,
as well as looking up the rule to be evaluated.To facilitate functions likerule(), use the@policies.want_contextdecorator.  Thepolicies.PolicyContextobject will be passed as the first argument of the function, with
remaining arguments passed after that.  Note that all the arguments
will be popped off the stack, but the function’s return value willnotbe pushed on the stack; a function decorated with@policies.want_contextmust perform its own manipulation of the
stack.  For a function like this to push a return value on the stack,
and assuming that the context argument isctxt, the relevant code
would be:ctxt.stack.append("value")In instances where you’re using functions decorated with@policies.want_context, it may be necessary to perform some
application-specific initialization on thepolicies.PolicyContextclass, such as initializing a context attribute.  This may be done by
changing thepolicies.Policy.context_classsetting.  Ideally, this
would be on an instance ofpolicies.Policy, rather than altering
the class itself, i.e.:policy = policies.Policy(...)
policy.context_class = MyPolicyContextBe very careful using@policies.want_context.  Failing to push a
function return value onto the evaluation context stack could corrupt
the stack and cause a crash during rule evaluation.policiesInternalsThis section intended for developers interested in developing thepoliciespackage itself.Rule ParsingThe policy rules work by parsing the rule text, using a parser built
withpyparsing, into a sequence ofinstructions.  The
instructions are stored in postfix order; that is, an expression like
“1+2” would become a sequence of instructions that would first push
the value “1” onto a stack; then push the value “2” onto the stack;
then pop the top two values from the stack, add them, and push the
result onto the stack.  The instructions are all defined ininstructions.py, and the parser is defined inparser.py.  Thepolicies.Policy.evaluate()method simply constructs an evaluation
context (apolicies.policy.PolicyContextobject), then executes
the instructions.  Included in the instructions are instructions that
create apolicy.Authorizationobject and set up the authorization
attributes (if any were defined); this authorization object is then
returned.CachingCaching is used wherever possible to achieve the highest possible
efficiency.  Policy rules are compiled the first time they are
evaluated, and the instructions are then cached.  The results of an
entrypoint look-up are also cached, as are the results of calling
rules–in the example above:user == target or rule("is_admin") {{ payment=rule("is_admin"),
                                      name=user==target }}The “is_admin” rule will only be evaluated one time.  This cache is
stored in thepolicies.PolicyContextobject, in therule_cacheattribute.