From 9e4403035a9953c99117083e6373ae3c441a76b5 Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Tue, 12 Dec 2017 15:20:49 +0000 Subject: Import py-macaroon-bakery_1.1.0.orig.tar.gz --- macaroonbakery/checkers/__init__.py | 18 +-- macaroonbakery/checkers/_auth_context.py | 58 ++++++++ macaroonbakery/checkers/_caveat.py | 128 ++++++++++++++++ macaroonbakery/checkers/_checkers.py | 246 +++++++++++++++++++++++++++++++ macaroonbakery/checkers/_conditions.py | 17 +++ macaroonbakery/checkers/_declared.py | 84 +++++++++++ macaroonbakery/checkers/_namespace.py | 165 +++++++++++++++++++++ macaroonbakery/checkers/_operation.py | 17 +++ macaroonbakery/checkers/_time.py | 67 +++++++++ macaroonbakery/checkers/_utils.py | 13 ++ macaroonbakery/checkers/auth_context.py | 58 -------- macaroonbakery/checkers/caveat.py | 125 ---------------- macaroonbakery/checkers/checkers.py | 243 ------------------------------ macaroonbakery/checkers/conditions.py | 17 --- macaroonbakery/checkers/declared.py | 82 ----------- macaroonbakery/checkers/namespace.py | 165 --------------------- macaroonbakery/checkers/operation.py | 17 --- macaroonbakery/checkers/time.py | 67 --------- macaroonbakery/checkers/utils.py | 13 -- 19 files changed, 804 insertions(+), 796 deletions(-) create mode 100644 macaroonbakery/checkers/_auth_context.py create mode 100644 macaroonbakery/checkers/_caveat.py create mode 100644 macaroonbakery/checkers/_checkers.py create mode 100644 macaroonbakery/checkers/_conditions.py create mode 100644 macaroonbakery/checkers/_declared.py create mode 100644 macaroonbakery/checkers/_namespace.py create mode 100644 macaroonbakery/checkers/_operation.py create mode 100644 macaroonbakery/checkers/_time.py create mode 100644 macaroonbakery/checkers/_utils.py delete mode 100644 macaroonbakery/checkers/auth_context.py delete mode 100644 macaroonbakery/checkers/caveat.py delete mode 100644 macaroonbakery/checkers/checkers.py delete mode 100644 macaroonbakery/checkers/conditions.py delete mode 100644 macaroonbakery/checkers/declared.py delete mode 100644 macaroonbakery/checkers/namespace.py delete mode 100644 macaroonbakery/checkers/operation.py delete mode 100644 macaroonbakery/checkers/time.py delete mode 100644 macaroonbakery/checkers/utils.py (limited to 'macaroonbakery/checkers') diff --git a/macaroonbakery/checkers/__init__.py b/macaroonbakery/checkers/__init__.py index 25c6b7d..b3ea466 100644 --- a/macaroonbakery/checkers/__init__.py +++ b/macaroonbakery/checkers/__init__.py @@ -1,6 +1,6 @@ # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. -from macaroonbakery.checkers.conditions import ( +from ._conditions import ( STD_NAMESPACE, COND_DECLARED, COND_TIME_BEFORE, @@ -9,7 +9,7 @@ from macaroonbakery.checkers.conditions import ( COND_DENY, COND_NEED_DECLARED, ) -from macaroonbakery.checkers.caveat import ( +from ._caveat import ( allow_caveat, deny_caveat, declared_caveat, @@ -17,35 +17,35 @@ from macaroonbakery.checkers.caveat import ( time_before_caveat, Caveat, ) -from macaroonbakery.checkers.declared import ( +from ._declared import ( context_with_declared, infer_declared, infer_declared_from_conditions, need_declared_caveat, ) -from macaroonbakery.checkers.operation import ( +from ._operation import ( context_with_operations, ) -from macaroonbakery.checkers.namespace import ( +from ._namespace import ( Namespace, deserialize_namespace ) -from macaroonbakery.checkers.time import ( +from ._time import ( context_with_clock, expiry_time, macaroons_expiry_time, ) -from macaroonbakery.checkers.checkers import ( +from ._checkers import ( Checker, CheckerInfo, RegisterError, ) -from macaroonbakery.checkers.auth_context import ( +from ._auth_context import ( AuthContext, ContextKey, ) -from macaroonbakery.checkers.utils import ( +from ._utils import ( condition_with_prefix, ) 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..5732f43 --- /dev/null +++ b/macaroonbakery/checkers/_caveat.py @@ -0,0 +1,128 @@ +# Copyright 2017 Canonical Ltd. +# Licensed under the LGPLv3, see LICENCE file for details. +import collections + +import pyrfc3339 +from ._conditions import ( + COND_ALLOW, + COND_DECLARED, + COND_DENY, + COND_ERROR, + COND_TIME_BEFORE, + STD_NAMESPACE, +) + + +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..71cb56f --- /dev/null +++ b/macaroonbakery/checkers/_checkers.py @@ -0,0 +1,246 @@ +# 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 ._caveat import parse_caveat +from ._conditions import ( + COND_ALLOW, + COND_DECLARED, + COND_DENY, + COND_ERROR, + COND_TIME_BEFORE, + STD_NAMESPACE, +) +from ._declared import DECLARED_KEY +from ._namespace import Namespace +from ._operation import OP_KEY +from ._time import TIME_KEY +from ._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..ae4f95b --- /dev/null +++ b/macaroonbakery/checkers/_declared.py @@ -0,0 +1,84 @@ +# Copyright 2017 Canonical Ltd. +# Licensed under the LGPLv3, see LICENCE file for details. +from ._auth_context import ContextKey +from ._caveat import Caveat, error_caveat, parse_caveat +from ._conditions import ( + COND_DECLARED, + COND_NEED_DECLARED, + STD_NAMESPACE, +) +from ._namespace import Namespace + +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..6c3b1e3 --- /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 ._caveat import error_caveat +from ._utils import condition_with_prefix + + +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..56b267a --- /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 ._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..2ae1d89 --- /dev/null +++ b/macaroonbakery/checkers/_time.py @@ -0,0 +1,67 @@ +# Copyright 2017 Canonical Ltd. +# Licensed under the LGPLv3, see LICENCE file for details. + +import pyrfc3339 +from ._auth_context import ContextKey +from ._caveat import parse_caveat +from ._conditions import COND_TIME_BEFORE, STD_NAMESPACE +from ._utils import condition_with_prefix + +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) + + +def macaroons_expiry_time(ns, ms): + ''' Returns the minimum time of any time-before caveats found in the given + macaroons or None if no such caveats were found. + :param ns: a Namespace, used to resolve caveats. + :param ms: a list of pymacaroons.Macaroon + :return: datetime.DateTime or None. + ''' + t = None + for m in ms: + et = expiry_time(ns, m.caveats) + if et is not None and (t is None or et < t): + t = et + return t + + +def expiry_time(ns, cavs): + ''' Returns the minimum time of any time-before caveats found + in the given list or None if no such caveats were found. + + The ns parameter is + :param ns: used to determine the standard namespace prefix - if + the standard namespace is not found, the empty prefix is assumed. + :param cavs: a list of pymacaroons.Caveat + :return: datetime.DateTime or None. + ''' + prefix = ns.resolve(STD_NAMESPACE) + time_before_cond = condition_with_prefix( + prefix, COND_TIME_BEFORE) + t = None + for cav in cavs: + if not cav.first_party(): + continue + cav = cav.caveat_id_bytes.decode('utf-8') + name, rest = parse_caveat(cav) + if name != time_before_cond: + continue + try: + et = pyrfc3339.parse(rest, utc=True).replace(tzinfo=None) + if t is None or et < t: + t = et + except ValueError: + continue + return t diff --git a/macaroonbakery/checkers/_utils.py b/macaroonbakery/checkers/_utils.py new file mode 100644 index 0000000..925e8c7 --- /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 == '' or prefix is None: + return condition + + return prefix + ':' + condition diff --git a/macaroonbakery/checkers/auth_context.py b/macaroonbakery/checkers/auth_context.py deleted file mode 100644 index dceb015..0000000 --- a/macaroonbakery/checkers/auth_context.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 deleted file mode 100644 index a1e564e..0000000 --- a/macaroonbakery/checkers/caveat.py +++ /dev/null @@ -1,125 +0,0 @@ -# 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 deleted file mode 100644 index 776b50b..0000000 --- a/macaroonbakery/checkers/checkers.py +++ /dev/null @@ -1,243 +0,0 @@ -# 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 deleted file mode 100644 index 74e863e..0000000 --- a/macaroonbakery/checkers/conditions.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 deleted file mode 100644 index 78a6181..0000000 --- a/macaroonbakery/checkers/declared.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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 deleted file mode 100644 index 31e8801..0000000 --- a/macaroonbakery/checkers/namespace.py +++ /dev/null @@ -1,165 +0,0 @@ -# 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 deleted file mode 100644 index a3b3805..0000000 --- a/macaroonbakery/checkers/operation.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 deleted file mode 100644 index 0b52131..0000000 --- a/macaroonbakery/checkers/time.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2017 Canonical Ltd. -# Licensed under the LGPLv3, see LICENCE file for details. - -import pyrfc3339 - -from macaroonbakery.checkers.auth_context import ContextKey -from macaroonbakery.checkers.conditions import COND_TIME_BEFORE, STD_NAMESPACE -from macaroonbakery.checkers.utils import condition_with_prefix -from macaroonbakery.checkers.caveat import parse_caveat - - -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) - - -def macaroons_expiry_time(ns, ms): - ''' Returns the minimum time of any time-before caveats found in the given - macaroons or None if no such caveats were found. - :param ns: a Namespace, used to resolve caveats. - :param ms: a list of pymacaroons.Macaroon - :return: datetime.DateTime or None. - ''' - t = None - for m in ms: - et = expiry_time(ns, m.caveats) - if et is not None and (t is None or et < t): - t = et - return t - - -def expiry_time(ns, cavs): - ''' Returns the minimum time of any time-before caveats found - in the given list or None if no such caveats were found. - - The ns parameter is - :param ns: used to determine the standard namespace prefix - if - the standard namespace is not found, the empty prefix is assumed. - :param cavs: a list of pymacaroons.Caveat - :return: datetime.DateTime or None. - ''' - prefix = ns.resolve(STD_NAMESPACE) - time_before_cond = condition_with_prefix( - prefix, COND_TIME_BEFORE) - t = None - for cav in cavs: - cav = cav.caveat_id_bytes.decode('utf-8') - name, rest = parse_caveat(cav) - if name != time_before_cond: - continue - try: - et = pyrfc3339.parse(rest) - if t is None or et < t: - t = et - except ValueError: - continue - return t diff --git a/macaroonbakery/checkers/utils.py b/macaroonbakery/checkers/utils.py deleted file mode 100644 index 925e8c7..0000000 --- a/macaroonbakery/checkers/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 == '' or prefix is None: - return condition - - return prefix + ':' + condition -- cgit v1.2.3