summaryrefslogtreecommitdiff
path: root/macaroonbakery/checkers
diff options
context:
space:
mode:
Diffstat (limited to 'macaroonbakery/checkers')
-rw-r--r--macaroonbakery/checkers/__init__.py50
-rw-r--r--macaroonbakery/checkers/auth_context.py58
-rw-r--r--macaroonbakery/checkers/caveat.py125
-rw-r--r--macaroonbakery/checkers/checkers.py243
-rw-r--r--macaroonbakery/checkers/conditions.py17
-rw-r--r--macaroonbakery/checkers/declared.py82
-rw-r--r--macaroonbakery/checkers/namespace.py165
-rw-r--r--macaroonbakery/checkers/operation.py17
-rw-r--r--macaroonbakery/checkers/time.py18
-rw-r--r--macaroonbakery/checkers/utils.py13
10 files changed, 788 insertions, 0 deletions
diff --git a/macaroonbakery/checkers/__init__.py b/macaroonbakery/checkers/__init__.py
new file mode 100644
index 0000000..9f0b022
--- /dev/null
+++ b/macaroonbakery/checkers/__init__.py
@@ -0,0 +1,50 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from macaroonbakery.checkers.conditions import (
+ STD_NAMESPACE, COND_DECLARED, COND_TIME_BEFORE, COND_ERROR, COND_ALLOW,
+ COND_DENY, COND_NEED_DECLARED
+)
+from macaroonbakery.checkers.caveat import (
+ allow_caveat, deny_caveat, declared_caveat, parse_caveat,
+ time_before_caveat, Caveat
+)
+from macaroonbakery.checkers.declared import (
+ context_with_declared, infer_declared, infer_declared_from_conditions,
+ need_declared_caveat
+)
+from macaroonbakery.checkers.operation import context_with_operations
+from macaroonbakery.checkers.namespace import Namespace, deserialize_namespace
+from macaroonbakery.checkers.time import context_with_clock
+from macaroonbakery.checkers.checkers import (
+ Checker, CheckerInfo, RegisterError
+)
+from macaroonbakery.checkers.auth_context import AuthContext, ContextKey
+
+__all__ = [
+ 'AuthContext',
+ 'Caveat',
+ 'Checker',
+ 'CheckerInfo',
+ 'COND_ALLOW',
+ 'COND_DECLARED',
+ 'COND_DENY',
+ 'COND_ERROR',
+ 'COND_NEED_DECLARED',
+ 'COND_TIME_BEFORE',
+ 'ContextKey',
+ 'STD_NAMESPACE',
+ 'Namespace',
+ 'RegisterError',
+ 'allow_caveat',
+ 'context_with_declared',
+ 'context_with_operations',
+ 'context_with_clock',
+ 'declared_caveat',
+ 'deny_caveat',
+ 'deserialize_namespace',
+ 'infer_declared',
+ 'infer_declared_from_conditions',
+ 'need_declared_caveat',
+ 'parse_caveat',
+ 'time_before_caveat',
+]
diff --git a/macaroonbakery/checkers/auth_context.py b/macaroonbakery/checkers/auth_context.py
new file mode 100644
index 0000000..dceb015
--- /dev/null
+++ b/macaroonbakery/checkers/auth_context.py
@@ -0,0 +1,58 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import collections
+
+
+class AuthContext(collections.Mapping):
+ ''' Holds a set of keys and values relevant to authorization.
+
+ It is passed as an argument to authorization checkers, so that the checkers
+ can access information about the context of the authorization request.
+ It is immutable - values can only be added by copying the whole thing.
+ '''
+ def __init__(self, somedict=None):
+ if somedict is None:
+ somedict = {}
+ self._dict = dict(somedict)
+ self._hash = None
+
+ def with_value(self, key, val):
+ ''' Return a copy of the AuthContext object with the given key and
+ value added.
+ '''
+ new_dict = dict(self._dict)
+ new_dict[key] = val
+ return AuthContext(new_dict)
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ def __len__(self):
+ return len(self._dict)
+
+ def __iter__(self):
+ return iter(self._dict)
+
+ def __hash__(self):
+ if self._hash is None:
+ self._hash = hash(frozenset(self._dict.items()))
+ return self._hash
+
+ def __eq__(self, other):
+ return self._dict == other._dict
+
+
+class ContextKey(object):
+ '''Provides a unique key suitable for use as a key into AuthContext.'''
+
+ def __init__(self, name):
+ '''Creates a context key using the given name. The name is
+ only for informational purposes.
+ '''
+ self._name = name
+
+ def __str__(self):
+ return '%s#%#x' % (self._name, id(self))
+
+ def __repr__(self):
+ return 'context_key(%r, %#x)' % (self._name, id(self))
diff --git a/macaroonbakery/checkers/caveat.py b/macaroonbakery/checkers/caveat.py
new file mode 100644
index 0000000..a1e564e
--- /dev/null
+++ b/macaroonbakery/checkers/caveat.py
@@ -0,0 +1,125 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import collections
+
+import pyrfc3339
+
+from macaroonbakery.checkers.conditions import (
+ STD_NAMESPACE, COND_TIME_BEFORE, COND_ERROR, COND_DENY, COND_ALLOW,
+ COND_DECLARED
+)
+
+
+class Caveat(collections.namedtuple('Caveat', 'condition location namespace')):
+ '''Represents a condition that must be true for a check to complete
+ successfully.
+
+ If location is provided, the caveat must be discharged by
+ a third party at the given location (a URL string).
+
+ The namespace parameter holds the namespace URI string of the
+ condition - if it is provided, it will be converted to a namespace prefix
+ before adding to the macaroon.
+ '''
+ __slots__ = ()
+
+ def __new__(cls, condition, location=None, namespace=None):
+ return super(Caveat, cls).__new__(cls, condition, location, namespace)
+
+
+def declared_caveat(key, value):
+ '''Returns a "declared" caveat asserting that the given key is
+ set to the given value.
+
+ If a macaroon has exactly one first party caveat asserting the value of a
+ particular key, then infer_declared will be able to infer the value, and
+ then the check will allow the declared value if it has the value
+ specified here.
+
+ If the key is empty or contains a space, it will return an error caveat.
+ '''
+ if key.find(' ') >= 0 or key == '':
+ return error_caveat('invalid caveat \'declared\' key "{}"'.format(key))
+ return _first_party(COND_DECLARED, key + ' ' + value)
+
+
+def error_caveat(f):
+ '''Returns a caveat that will never be satisfied, holding f as the text of
+ the caveat.
+
+ This should only be used for highly unusual conditions that are never
+ expected to happen in practice, such as a malformed key that is
+ conventionally passed as a constant. It's not a panic but you should
+ only use it in cases where a panic might possibly be appropriate.
+
+ This mechanism means that caveats can be created without error
+ checking and a later systematic check at a higher level (in the
+ bakery package) can produce an error instead.
+ '''
+ return _first_party(COND_ERROR, f)
+
+
+def allow_caveat(ops):
+ ''' Returns a caveat that will deny attempts to use the macaroon to perform
+ any operation other than those listed. Operations must not contain a space.
+ '''
+ if ops is None or len(ops) == 0:
+ return error_caveat('no operations allowed')
+ return _operation_caveat(COND_ALLOW, ops)
+
+
+def deny_caveat(ops):
+ '''Returns a caveat that will deny attempts to use the macaroon to perform
+ any of the listed operations. Operations must not contain a space.
+ '''
+ return _operation_caveat(COND_DENY, ops)
+
+
+def _operation_caveat(cond, ops):
+ ''' Helper for allow_caveat and deny_caveat.
+
+ It checks that all operation names are valid before creating the caveat.
+ '''
+ for op in ops:
+ if op.find(' ') != -1:
+ return error_caveat('invalid operation name "{}"'.format(op))
+ return _first_party(cond, ' '.join(ops))
+
+
+def time_before_caveat(t):
+ '''Return a caveat that specifies that the time that it is checked at
+ should be before t.
+ :param t is a a UTC date in - use datetime.utcnow, not datetime.now
+ '''
+
+ return _first_party(COND_TIME_BEFORE,
+ pyrfc3339.generate(t, accept_naive=True,
+ microseconds=True))
+
+
+def parse_caveat(cav):
+ ''' Parses a caveat into an identifier, identifying the checker that should
+ be used, and the argument to the checker (the rest of the string).
+
+ The identifier is taken from all the characters before the first
+ space character.
+ :return two string, identifier and arg
+ '''
+ if cav == '':
+ raise ValueError('empty caveat')
+ try:
+ i = cav.index(' ')
+ except ValueError:
+ return cav, ''
+ if i == 0:
+ raise ValueError('caveat starts with space character')
+ return cav[0:i], cav[i + 1:]
+
+
+def _first_party(name, arg):
+ condition = name
+ if arg != '':
+ condition += ' ' + arg
+
+ return Caveat(condition=condition,
+ namespace=STD_NAMESPACE)
diff --git a/macaroonbakery/checkers/checkers.py b/macaroonbakery/checkers/checkers.py
new file mode 100644
index 0000000..776b50b
--- /dev/null
+++ b/macaroonbakery/checkers/checkers.py
@@ -0,0 +1,243 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import abc
+from collections import namedtuple
+from datetime import datetime
+
+import pyrfc3339
+import pytz
+
+from macaroonbakery.checkers.declared import DECLARED_KEY
+from macaroonbakery.checkers.time import TIME_KEY
+from macaroonbakery.checkers.operation import OP_KEY
+from macaroonbakery.checkers.namespace import Namespace
+from macaroonbakery.checkers.caveat import parse_caveat
+from macaroonbakery.checkers.conditions import (
+ STD_NAMESPACE, COND_DECLARED, COND_ALLOW, COND_DENY, COND_ERROR,
+ COND_TIME_BEFORE
+)
+from macaroonbakery.checkers.utils import condition_with_prefix
+
+
+class RegisterError(Exception):
+ '''Raised when a condition cannot be registered with a Checker.'''
+ pass
+
+
+class FirstPartyCaveatChecker(object):
+ '''Used to check first party caveats for validity with respect to
+ information in the provided context.
+
+ If the caveat kind was not recognised, the checker should return
+ ErrCaveatNotRecognized.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def check_first_party_caveat(self, ctx, caveat):
+ ''' Checks that the given caveat condition is valid with respect to
+ the given context information.
+ :param ctx: an Auth context
+ :param caveat a string
+ '''
+ raise NotImplementedError('check_first_party_caveat method must be '
+ 'defined in subclass')
+
+ def namespace(self):
+ ''' Returns the namespace associated with the caveat checker.
+ '''
+ raise NotImplementedError('namespace method must be '
+ 'defined in subclass')
+
+
+class Checker(FirstPartyCaveatChecker):
+ ''' Holds a set of checkers for first party caveats.
+ '''
+
+ def __init__(self, namespace=None, include_std_checkers=True):
+ if namespace is None:
+ namespace = Namespace()
+ self._namespace = namespace
+ self._checkers = {}
+ if include_std_checkers:
+ self.register_std()
+
+ def check_first_party_caveat(self, ctx, cav):
+ ''' Checks the caveat against all registered caveat conditions.
+ :return: error message string if any or None
+ '''
+ try:
+ cond, arg = parse_caveat(cav)
+ except ValueError as ex:
+ # If we can't parse it, perhaps it's in some other format,
+ # return a not-recognised error.
+ return 'cannot parse caveat "{}": {}'.format(cav, ex.args[0])
+ checker = self._checkers.get(cond)
+ if checker is None:
+ return 'caveat "{}" not satisfied: caveat not recognized'.format(
+ cav)
+ err = checker.check(ctx, cond, arg)
+ if err is not None:
+ return 'caveat "{}" not satisfied: {}'.format(cav, err)
+
+ def namespace(self):
+ ''' Returns the namespace associated with the Checker.
+ '''
+ return self._namespace
+
+ def info(self):
+ ''' Returns information on all the registered checkers.
+
+ Sorted by namespace and then name
+ :returns a list of CheckerInfo
+ '''
+ return sorted(self._checkers.values(), key=lambda x: (x.ns, x.name))
+
+ def register(self, cond, uri, check):
+ ''' Registers the given condition(string) in the given namespace
+ uri (string) to be checked with the given check function.
+ The check function checks a caveat by passing an auth context, a cond
+ parameter(string) that holds the caveat condition including any
+ namespace prefix and an arg parameter(string) that hold any additional
+ caveat argument text. It will return any error as string otherwise
+ None.
+
+ It will raise a ValueError if the namespace is not registered or
+ if the condition has already been registered.
+ '''
+ if check is None:
+ raise RegisterError(
+ 'no check function registered for namespace {} when '
+ 'registering condition {}'.format(uri, cond))
+
+ prefix = self._namespace.resolve(uri)
+ if prefix is None:
+ raise RegisterError('no prefix registered for namespace {} when '
+ 'registering condition {}'.format(uri, cond))
+
+ if prefix == '' and cond.find(':') >= 0:
+ raise RegisterError(
+ 'caveat condition {} in namespace {} contains a colon but its'
+ ' prefix is empty'.format(cond, uri))
+
+ full_cond = condition_with_prefix(prefix, cond)
+ info = self._checkers.get(full_cond)
+ if info is not None:
+ raise RegisterError(
+ 'checker for {} (namespace {}) already registered in '
+ 'namespace {}'.format(full_cond, uri, info.ns))
+ self._checkers[full_cond] = CheckerInfo(
+ check=check,
+ ns=uri,
+ name=cond,
+ prefix=prefix)
+
+ def register_std(self):
+ ''' Registers all the standard checkers in the given checker.
+
+ If not present already, the standard checkers schema (STD_NAMESPACE) is
+ added to the checker's namespace with an empty prefix.
+ '''
+ self._namespace.register(STD_NAMESPACE, '')
+ for cond in _ALL_CHECKERS:
+ self.register(cond, STD_NAMESPACE, _ALL_CHECKERS[cond])
+
+
+class CheckerInfo(namedtuple('CheckInfo', 'prefix name ns check')):
+ '''CheckerInfo holds information on a registered checker.
+ '''
+ __slots__ = ()
+
+ def __new__(cls, prefix, name, ns, check=None):
+ '''
+ :param check holds the actual checker function which takes an auth
+ context and a condition and arg string as arguments.
+ :param prefix holds the prefix for the checker condition as string.
+ :param name holds the name of the checker condition as string.
+ :param ns holds the namespace URI for the checker's schema as
+ Namespace.
+ '''
+ return super(CheckerInfo, cls).__new__(cls, prefix, name, ns, check)
+
+
+def _check_time_before(ctx, cond, arg):
+ clock = ctx.get(TIME_KEY)
+ if clock is None:
+ now = pytz.UTC.localize(datetime.utcnow())
+ else:
+ now = clock.utcnow()
+
+ try:
+ if pyrfc3339.parse(arg) <= now:
+ return 'macaroon has expired'
+ except ValueError:
+ return 'cannot parse "{}" as RFC 3339'.format(arg)
+ return None
+
+
+def _check_declared(ctx, cond, arg):
+ parts = arg.split(' ', 1)
+ if len(parts) != 2:
+ return 'declared caveat has no value'
+ attrs = ctx.get(DECLARED_KEY, {})
+ val = attrs.get(parts[0])
+ if val is None:
+ return 'got {}=null, expected "{}"'.format(parts[0], parts[1])
+
+ if val != parts[1]:
+ return 'got {}="{}", expected "{}"'.format(parts[0], val, parts[1])
+ return None
+
+
+def _check_error(ctx, cond, arg):
+ return 'bad caveat'
+
+
+def _check_allow(ctx, cond, arg):
+ return _check_operations(ctx, True, arg)
+
+
+def _check_deny(ctx, cond, arg):
+ return _check_operations(ctx, False, arg)
+
+
+def _check_operations(ctx, need_ops, arg):
+ ''' Checks an allow or a deny caveat. The need_ops parameter specifies
+ whether we require all the operations in the caveat to be declared in
+ the context.
+ '''
+ ctx_ops = ctx.get(OP_KEY, [])
+ if len(ctx_ops) == 0:
+ if need_ops:
+ f = arg.split()
+ if len(f) == 0:
+ return 'no operations allowed'
+ return '{} not allowed'.format(f[0])
+ return None
+
+ fields = arg.split()
+ for op in ctx_ops:
+ err = _check_op(op, need_ops, fields)
+ if err is not None:
+ return err
+ return None
+
+
+def _check_op(ctx_op, need_op, fields):
+ found = False
+ for op in fields:
+ if op == ctx_op:
+ found = True
+ break
+ if found != need_op:
+ return '{} not allowed'.format(ctx_op)
+ return None
+
+
+_ALL_CHECKERS = {
+ COND_TIME_BEFORE: _check_time_before,
+ COND_DECLARED: _check_declared,
+ COND_ERROR: _check_error,
+ COND_ALLOW: _check_allow,
+ COND_DENY: _check_deny,
+}
diff --git a/macaroonbakery/checkers/conditions.py b/macaroonbakery/checkers/conditions.py
new file mode 100644
index 0000000..74e863e
--- /dev/null
+++ b/macaroonbakery/checkers/conditions.py
@@ -0,0 +1,17 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+
+# StdNamespace holds the URI of the standard checkers schema.
+STD_NAMESPACE = 'std'
+
+# Constants for all the standard caveat conditions.
+# First and third party caveat conditions are both defined here,
+# even though notionally they exist in separate name spaces.
+COND_DECLARED = 'declared'
+COND_TIME_BEFORE = 'time-before'
+COND_ERROR = 'error'
+COND_ALLOW = 'allow'
+COND_DENY = 'deny'
+
+
+COND_NEED_DECLARED = 'need-declared'
diff --git a/macaroonbakery/checkers/declared.py b/macaroonbakery/checkers/declared.py
new file mode 100644
index 0000000..78a6181
--- /dev/null
+++ b/macaroonbakery/checkers/declared.py
@@ -0,0 +1,82 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from macaroonbakery.checkers.namespace import Namespace
+from macaroonbakery.checkers.caveat import parse_caveat, Caveat, error_caveat
+from macaroonbakery.checkers.conditions import (
+ COND_DECLARED, COND_NEED_DECLARED, STD_NAMESPACE
+)
+from macaroonbakery.checkers.auth_context import ContextKey
+
+DECLARED_KEY = ContextKey('declared-key')
+
+
+def infer_declared(ms, namespace=None):
+ '''Retrieves any declared information from the given macaroons and returns
+ it as a key-value map.
+ Information is declared with a first party caveat as created by
+ declared_caveat.
+
+ If there are two caveats that declare the same key with different values,
+ the information is omitted from the map. When the caveats are later
+ checked, this will cause the check to fail.
+ namespace is the Namespace used to retrieve the prefix associated to the
+ uri, if None it will use the STD_NAMESPACE only.
+ '''
+ conditions = []
+ for m in ms:
+ for cav in m.caveats:
+ if cav.location is None or cav.location == '':
+ conditions.append(cav.caveat_id_bytes.decode('utf-8'))
+ return infer_declared_from_conditions(conditions, namespace)
+
+
+def infer_declared_from_conditions(conds, namespace=None):
+ ''' like infer_declared except that it is passed a set of first party
+ caveat conditions as a list of string rather than a set of macaroons.
+ '''
+ conflicts = []
+ # If we can't resolve that standard namespace, then we'll look for
+ # just bare "declared" caveats which will work OK for legacy
+ # macaroons with no namespace.
+ if namespace is None:
+ namespace = Namespace()
+ prefix = namespace.resolve(STD_NAMESPACE)
+ if prefix is None:
+ prefix = ''
+ declared_cond = prefix + COND_DECLARED
+
+ info = {}
+ for cond in conds:
+ try:
+ name, rest = parse_caveat(cond)
+ except ValueError:
+ name, rest = '', ''
+ if name != declared_cond:
+ continue
+ parts = rest.split(' ', 1)
+ if len(parts) != 2:
+ continue
+ key, val = parts[0], parts[1]
+ old_val = info.get(key)
+ if old_val is not None and old_val != val:
+ conflicts.append(key)
+ continue
+ info[key] = val
+ for key in set(conflicts):
+ del info[key]
+ return info
+
+
+def context_with_declared(ctx, declared):
+ ''' Returns a context with attached declared information,
+ as returned from infer_declared.
+ '''
+ return ctx.with_value(DECLARED_KEY, declared)
+
+
+def need_declared_caveat(cav, keys):
+ if cav.location == '':
+ return error_caveat('need-declared caveat is not third-party')
+ return Caveat(location=cav.location,
+ condition=(COND_NEED_DECLARED + ' ' + ','.join(keys)
+ + ' ' + cav.condition))
diff --git a/macaroonbakery/checkers/namespace.py b/macaroonbakery/checkers/namespace.py
new file mode 100644
index 0000000..31e8801
--- /dev/null
+++ b/macaroonbakery/checkers/namespace.py
@@ -0,0 +1,165 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import collections
+
+from macaroonbakery.checkers.utils import condition_with_prefix
+from macaroonbakery.checkers.caveat import error_caveat
+
+
+class Namespace:
+ '''Holds maps from schema URIs to prefixes.
+
+ prefixes that are used to encode them in first party
+ caveats. Several different URIs may map to the same
+ prefix - this is usual when several different backwardly
+ compatible schema versions are registered.
+ '''
+
+ def __init__(self, uri_to_prefix=None):
+ self._uri_to_prefix = {}
+ if uri_to_prefix is not None:
+ for k in uri_to_prefix:
+ self.register(k, uri_to_prefix[k])
+
+ def __str__(self):
+ '''Returns the namespace representation as returned by serialize
+ :return: str
+ '''
+ return self.serialize_text().decode('utf-8')
+
+ def __eq__(self, other):
+ return self._uri_to_prefix == other._uri_to_prefix
+
+ def serialize_text(self):
+ '''Returns a serialized form of the Namepace.
+
+ All the elements in the namespace are sorted by
+ URI, joined to the associated prefix with a colon and
+ separated with spaces.
+ :return: bytes
+ '''
+ if self._uri_to_prefix is None or len(self._uri_to_prefix) == 0:
+ return b''
+ od = collections.OrderedDict(sorted(self._uri_to_prefix.items()))
+ data = []
+ for uri in od:
+ data.append(uri + ':' + od[uri])
+ return ' '.join(data).encode('utf-8')
+
+ def register(self, uri, prefix):
+ '''Registers the given URI and associates it with the given prefix.
+
+ If the URI has already been registered, this is a no-op.
+
+ :param uri: string
+ :param prefix: string
+ '''
+ if not is_valid_schema_uri(uri):
+ raise KeyError(
+ 'cannot register invalid URI {} (prefix {})'.format(
+ uri, prefix))
+ if not is_valid_prefix(prefix):
+ raise ValueError(
+ 'cannot register invalid prefix %q for URI %q'.format(
+ prefix, uri))
+ if self._uri_to_prefix.get(uri) is None:
+ self._uri_to_prefix[uri] = prefix
+
+ def resolve(self, uri):
+ ''' Returns the prefix associated to the uri.
+
+ returns None if not found.
+ :param uri: string
+ :return: string
+ '''
+ return self._uri_to_prefix.get(uri)
+
+ def resolve_caveat(self, cav):
+ ''' Resolves the given caveat(string) by using resolve to map from its
+ schema namespace to the appropriate prefix.
+ If there is no registered prefix for the namespace, it returns an error
+ caveat.
+ If cav.namespace is empty or cav.location is non-empty, it returns cav
+ unchanged.
+
+ It does not mutate ns and may be called concurrently with other
+ non-mutating Namespace methods.
+ :return: Caveat object
+ '''
+ # TODO: If a namespace isn't registered, try to resolve it by
+ # resolving it to the latest compatible version that is
+ # registered.
+ if cav.namespace == '' or cav.location != '':
+ return cav
+
+ prefix = self.resolve(cav.namespace)
+ if prefix is None:
+ err_cav = error_caveat(
+ 'caveat {} in unregistered namespace {}'.format(
+ cav.condition, cav.namespace))
+ if err_cav.namespace != cav.namespace:
+ prefix = self.resolve(err_cav.namespace)
+ if prefix is None:
+ prefix = ''
+ cav = err_cav
+ if prefix != '':
+ cav.condition = condition_with_prefix(prefix, cav.condition)
+ cav.namespace = ''
+ return cav
+
+
+def is_valid_schema_uri(uri):
+ '''Reports if uri is suitable for use as a namespace schema URI.
+
+ It must be non-empty and it must not contain white space.
+
+ :param uri string
+ :return bool
+ '''
+ if len(uri) <= 0:
+ return False
+ return uri.find(' ') == -1
+
+
+def is_valid_prefix(prefix):
+ '''Reports if prefix is valid.
+
+ It must not contain white space or semi-colon.
+ :param prefix string
+ :return bool
+ '''
+ return prefix.find(' ') == -1 and prefix.find(':') == -1
+
+
+def deserialize_namespace(data):
+ ''' Deserialize a Namespace object.
+
+ :param data: bytes or str
+ :return: namespace
+ '''
+ if isinstance(data, bytes):
+ data = data.decode('utf-8')
+ kvs = data.split()
+ uri_to_prefix = {}
+ for kv in kvs:
+ i = kv.rfind(':')
+ if i == -1:
+ raise ValueError('no colon in namespace '
+ 'field {}'.format(repr(kv)))
+ uri, prefix = kv[0:i], kv[i + 1:]
+ if not is_valid_schema_uri(uri):
+ # Currently this can't happen because the only invalid URIs
+ # are those which contain a space
+ raise ValueError(
+ 'invalid URI {} in namespace '
+ 'field {}'.format(repr(uri), repr(kv)))
+ if not is_valid_prefix(prefix):
+ raise ValueError(
+ 'invalid prefix {} in namespace field'
+ ' {}'.format(repr(prefix), repr(kv)))
+ if uri in uri_to_prefix:
+ raise ValueError(
+ 'duplicate URI {} in '
+ 'namespace {}'.format(repr(uri), repr(data)))
+ uri_to_prefix[uri] = prefix
+ return Namespace(uri_to_prefix)
diff --git a/macaroonbakery/checkers/operation.py b/macaroonbakery/checkers/operation.py
new file mode 100644
index 0000000..a3b3805
--- /dev/null
+++ b/macaroonbakery/checkers/operation.py
@@ -0,0 +1,17 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from macaroonbakery.checkers.auth_context import ContextKey
+
+OP_KEY = ContextKey('op-key')
+
+
+def context_with_operations(ctx, ops):
+ ''' Returns a context(AuthContext) which is associated with all the given
+ operations (list of string). It will be based on the auth context
+ passed in as ctx.
+
+ An allow caveat will succeed only if one of the allowed operations is in
+ ops; a deny caveat will succeed only if none of the denied operations are
+ in ops.
+ '''
+ return ctx.with_value(OP_KEY, ops)
diff --git a/macaroonbakery/checkers/time.py b/macaroonbakery/checkers/time.py
new file mode 100644
index 0000000..052d983
--- /dev/null
+++ b/macaroonbakery/checkers/time.py
@@ -0,0 +1,18 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from macaroonbakery.checkers.auth_context import ContextKey
+
+
+TIME_KEY = ContextKey('time-key')
+
+
+def context_with_clock(ctx, clock):
+ ''' Returns a copy of ctx with a key added that associates it with the given
+ clock implementation, which will be used by the time-before checker
+ to determine the current time.
+ The clock should have a utcnow method that returns the current time
+ as a datetime value in UTC.
+ '''
+ if clock is None:
+ return ctx
+ return ctx.with_value(TIME_KEY, clock)
diff --git a/macaroonbakery/checkers/utils.py b/macaroonbakery/checkers/utils.py
new file mode 100644
index 0000000..f2e51b1
--- /dev/null
+++ b/macaroonbakery/checkers/utils.py
@@ -0,0 +1,13 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+
+
+def condition_with_prefix(prefix, condition):
+ '''Returns the given string prefixed by the given prefix.
+
+ If the prefix is non-empty, a colon is used to separate them.
+ '''
+ if prefix == '':
+ return condition
+
+ return prefix + ':' + condition