diff options
author | Colin Watson <cjwatson@debian.org> | 2017-11-03 12:13:13 +0000 |
---|---|---|
committer | Colin Watson <cjwatson@debian.org> | 2017-11-03 12:13:13 +0000 |
commit | 3d9eaeb5dacee168a93da090e2c0d46eedbe51a2 (patch) | |
tree | 779d797fb3cf6cc9552cb08c40662b5d3d8397fd /macaroonbakery/checkers | |
parent | 79ff2842fa477ee0693ea167c0a74cd7cf080d27 (diff) |
Import py-macaroon-bakery_0.0.4.orig.tar.gz
Diffstat (limited to 'macaroonbakery/checkers')
-rw-r--r-- | macaroonbakery/checkers/__init__.py | 50 | ||||
-rw-r--r-- | macaroonbakery/checkers/auth_context.py | 58 | ||||
-rw-r--r-- | macaroonbakery/checkers/caveat.py | 125 | ||||
-rw-r--r-- | macaroonbakery/checkers/checkers.py | 243 | ||||
-rw-r--r-- | macaroonbakery/checkers/conditions.py | 17 | ||||
-rw-r--r-- | macaroonbakery/checkers/declared.py | 82 | ||||
-rw-r--r-- | macaroonbakery/checkers/namespace.py | 165 | ||||
-rw-r--r-- | macaroonbakery/checkers/operation.py | 17 | ||||
-rw-r--r-- | macaroonbakery/checkers/time.py | 18 | ||||
-rw-r--r-- | macaroonbakery/checkers/utils.py | 13 |
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 |