-
Notifications
You must be signed in to change notification settings - Fork 10
Access Control List Security Extension
Predicate-based security for your applications, extensions, and reusable components.
- This extension is available in:
WebCore>=2.0.3,<2.1.0
- This extension requires any version of Python.
- This extension has no external package dependencies.
- This extension is available with the name
acl
in theweb.ext
namespace. - This extension adds the following context attributes:
acl
- This extension uses namespaced plugins from:
web.acl.predicate
- Introduction 2. Operation 3. Predicates
- Usage
This extension provides a method of collecting rules from objects during dispatch descent and subsequently evaluating
them. This serves the purpose of an access cotrol list (ACL) by allowing these rules to grant access (return True
),
explicitly deny access (return False
), or abstain (return None
). Additionally, values returned from endpoints will
have the value of their __acl__
attribute evaluated, if present.
Also provided are a stock set of predicates which allow for basic boolean logic, various nesting patterns, and
provide building blocks for more complex behaviour. It is preferred to access these via the where
helper object,
whose attributes (also provided as a mapping) are the names of entry_points
registered plugins.
On any endpoint or object leading to an endpoint during dispatch, define an __acl__
attribute or property which
provides an iterable (set
, list
, tuple
, generator, etc.) of predicate objects. Objects may also optionally
specify an __acl_inherit__
attribute or property, which, if falsy, will clear the ACL that had been built so far
for the request.
After a final endpoint has been reached, these rules are evaluated in turn (using the First
predicate), passing the
request context as their first argument. Each is called until one either returns True
to indicate permission has
been explicitly granted, or returns False
to indicate permission has been explicitly denied. If no predicates decide
to have an opinion, the default action is configurable.
A predicate is any callable object that optionally accepts a context as its first positional parameter. One might look like:
def always(context=None):
return True
That's it, really. The provided built-in ones are class-based, but the process is the same even if the method has a
strange name like __call__
.
Before utilizing access control list functionality in your own application you must first enable the extension.
Regardless of configuration route chosen rules may be specified as strings, classes, or callables. Strings are
resolved using the web.acl.predicate
entry_point
namespace and further processed. Classes (either directly, or
loaded by plugin name) are instantiated without arguments and their instance used.
Applications using code to explicitly construct the WebCore Application
object, but with no particular custom base
ruleset needed, can pass the extension by name. It will be loaded using its entry_points
plugin reference.
app = Application("Hi.", extensions=['acl'])
Applications with a more strict configuration may wish to specify a default rule of never
. Import the extension
yourself, and specify a default rule.
from web.ext.acl import ACLExtension, when
app = Application("Hi.", extensions=[ACLExtension(default=when.never)])
More complex arrangements can be had by specifying rules positionally (their order will be preserved) or by passing a
policy
iterable by name. These may be combined with the default
named argument, with them being combined as
positional
+ policy
+ default
.
Using a JSON or YAML-based configuration, you would define your application's extensions
section either with the
bare extension declared:
application:
root: "Hi."
extensions:
acl:
Or, specify a default policy by treating the acl
entry as an array:
application:
root: "Hi."
extensions:
acl:
- never
By specifying a singular default
explicitly:
application:
root: "Hi."
extensions:
acl:
default: never
Or, finally, by specifying the policy
, which must be an array, explicitly:
application:
root: "Hi."
extensions:
acl:
policy:
- never
Use of policy
and default
may be combined, with the default appended to the given policy.
Note: We'll be using object dispatch for these examples.
First, you're going to need to from web.ext.acl import when
to get easy access to predicates.
Define an iterable of predicates using the __acl__
attribute.
class PermissiveController:
__acl__ = [when.always]
def __init__(self, context):
pass
def __call__(self):
return "Hi."
For intermediary nodes in descent and return values, such as a "root controller with method" arrangement, you can
define an __acl__
attribute. The contents of this attribute is collected during processing of dispatch events.
Using the when
utility as a decorator or decorator generator.
@when(when.never)
class SecureController:
def __init__(self, context):
pass
@when(when.always, inherit=False)
def insecure_resource(self):
return "Yo."
def __call__(self):
return "Hi."
You can use the when
predicate accessor as a decorator, defining the predicates for an object as positional
parameters. The result of calling when
can be saved used later as a decorator by itself, or as a filter to set that
attribute on other objects.
Controlling access to information, not just endpoints.
class Thing:
__acl__ = [when.never]
def endpoint(context):
return Thing()
In this example, Thing
will not allow itself to be returned by an endpoint. This process is not recursive.
Defining new predicates is fairly straightforward given the very simple interface. However, because many demands of predicates can be resolved entirely by comparison against a value from the request context, two predicate factories are provided. These can be used on-demand, or the result can be saved for repeated use later.
Grant or deny access based on a value from the context matching one of several possible values.
deny_console = when.matches(False, 'request.client_addr', None)
local = when.matches(True, 'request.remote_addr', '127.0.0.1', '::1')
@when(deny_console, local, when.matches(True, 'user.admin', True))
def endpoint(context):
return "Hi."
This will grant access to local users and those with the user.admin
flag equal to True
, as retrieved from the
context. The local
predicate has been saved for subsequent use, and demonstrates comparing against any of a number
of allowable values. The first argument is the intended predicate result if a match is made, the second is the value
to traverse and compare, and any remaining arguments are treated as acceptable values.
Grant or deny access based on a value from the context containing one of several possible values.
role = when.contains.partial(True, 'user.role')
@when(role('admin', 'editor'))
def endpoint(context):
return "Hi."
This allows you to easily compare against containers such as lists and sets. Also demonstrataed is the ability to "partially apply" a predicate, that is, apply some arguments, then apply the rest later.