Skip to content

Access Control List Security Extension

Alice Zoë Bevan–McGregor edited this page Sep 26, 2016 · 13 revisions

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 the web.ext namespace.
  • This extension adds the following context attributes:
    • acl
  • This extension uses namespaced plugins from:
    • web.acl.predicate
  1. Introduction 2. Operation 3. Predicates
  2. Usage
    1. Enabling the Extension
      1. Imperative Configuration
      2. Declarative Configuration
    2. Defining ACLs
      1. Explicit Usage
      2. Decoration
      3. Endpoint Return Values

Introduction

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.

Operation

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.

Predicates

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__.

Usage

Enabling the Extension

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.

Imperative Configuration

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.

Declarative Configuration

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.

Defining ACLs

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.

Explicit Usage

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.

Decoration

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.

Endpoint Return Values

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.

Extending

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.

Context Value Matches

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.

Context Value Contains

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.