[Python-modules-commits] [py-macaroon-bakery] 01/05: Import py-macaroon-bakery_0.0.4.orig.tar.gz

Colin Watson cjwatson at moszumanska.debian.org
Fri Nov 3 16:17:10 UTC 2017


This is an automated email from the git hooks/post-receive script.

cjwatson pushed a commit to branch master
in repository py-macaroon-bakery.

commit 3d9eaeb5dacee168a93da090e2c0d46eedbe51a2
Author: Colin Watson <cjwatson at debian.org>
Date:   Fri Nov 3 12:13:13 2017 +0000

    Import py-macaroon-bakery_0.0.4.orig.tar.gz
---
 Makefile                                       |   2 +-
 docs/conf.py                                   |   2 +-
 macaroonbakery/__init__.py                     |  95 ++-
 macaroonbakery/authorizer.py                   | 107 +++
 macaroonbakery/bakery.py                       | 148 ++--
 macaroonbakery/checker.py                      | 409 +++++++++++
 macaroonbakery/checkers.py                     |  23 -
 macaroonbakery/checkers/__init__.py            |  50 ++
 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     |  72 +-
 macaroonbakery/checkers/operation.py           |  17 +
 macaroonbakery/checkers/time.py                |  18 +
 macaroonbakery/checkers/utils.py               |  13 +
 macaroonbakery/codec.py                        | 148 ++--
 macaroonbakery/discharge.py                    | 210 ++++++
 macaroonbakery/error.py                        |  77 ++
 macaroonbakery/httpbakery/__init__.py          |  18 +-
 macaroonbakery/httpbakery/agent.py             |  11 +-
 macaroonbakery/httpbakery/client.py            |  26 +-
 macaroonbakery/httpbakery/error.py             |  67 ++
 macaroonbakery/httpbakery/keyring.py           |  56 ++
 macaroonbakery/identity.py                     | 126 ++++
 macaroonbakery/{tests => internal}/__init__.py |   0
 macaroonbakery/internal/id.proto               |  14 +
 macaroonbakery/internal/id_pb2.py              | 132 ++++
 macaroonbakery/json_serializer.py              |  75 --
 macaroonbakery/keys.py                         |  92 +++
 macaroonbakery/macaroon.py                     | 440 ++++++-----
 macaroonbakery/oven.py                         | 254 +++++++
 macaroonbakery/store.py                        |  77 ++
 macaroonbakery/tests/__init__.py               |   2 +
 macaroonbakery/tests/common.py                 | 120 +++
 macaroonbakery/tests/test_agent.py             |  13 +-
 macaroonbakery/tests/test_authorizer.py        | 132 ++++
 macaroonbakery/tests/test_checker.py           | 963 +++++++++++++++++++++++++
 macaroonbakery/tests/test_checkers.py          | 356 +++++++++
 macaroonbakery/tests/test_codec.py             | 164 +++--
 macaroonbakery/tests/test_discharge.py         | 445 ++++++++++++
 macaroonbakery/tests/test_discharge_all.py     | 170 +++++
 macaroonbakery/tests/test_keyring.py           | 111 +++
 macaroonbakery/tests/test_macaroon.py          | 230 ++++--
 macaroonbakery/tests/test_namespace.py         |  15 +-
 macaroonbakery/tests/test_oven.py              | 125 ++++
 macaroonbakery/tests/test_store.py             |  21 +
 macaroonbakery/third_party.py                  |  53 ++
 macaroonbakery/utils.py                        |  16 +-
 requirements.txt => macaroonbakery/versions.py |  10 +-
 requirements.txt                               |  11 +-
 setup.py                                       |  22 +-
 tox.ini                                        |   4 +-
 54 files changed, 5679 insertions(+), 608 deletions(-)

diff --git a/Makefile b/Makefile
index 4e87e5c..75d0e27 100644
--- a/Makefile
+++ b/Makefile
@@ -76,7 +76,7 @@ help:
 
 .PHONY: lint
 lint: setup
-	@$(DEVENV)/bin/flake8 --ignore E731 --show-source macaroonbakery
+	@$(DEVENV)/bin/flake8 --show-source macaroonbakery --exclude macaroonbakery/internal/id_pb2.py
 
 .PHONY: release
 release: check
diff --git a/docs/conf.py b/docs/conf.py
index 787735f..75593c5 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -63,7 +63,7 @@ copyright = u'2017, Juju UI Team'
 # the built documents.
 #
 # The short X.Y version and the full version.
-version = release = macaroonbakery.get_version()
+version = release = '0.0.4'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/macaroonbakery/__init__.py b/macaroonbakery/__init__.py
index 8020901..dd2e6df 100644
--- a/macaroonbakery/__init__.py
+++ b/macaroonbakery/__init__.py
@@ -9,9 +9,96 @@ except ImportError:
 else:
     urllib3.contrib.pyopenssl.inject_into_urllib3()
 
-VERSION = (0, 0, 3)
+from macaroonbakery.versions import (
+    LATEST_BAKERY_VERSION, BAKERY_V3, BAKERY_V2, BAKERY_V1, BAKERY_V0
+)
+from macaroonbakery.authorizer import (
+    ClosedAuthorizer, EVERYONE, AuthorizerFunc, Authorizer, ACLAuthorizer
+)
+from macaroonbakery.codec import (
+    encode_caveat, decode_caveat, encode_uvarint
+)
+from macaroonbakery.checker import (
+    Op, LOGIN_OP, AuthInfo, AuthChecker, Checker
+)
+from macaroonbakery.error import (
+    ThirdPartyCaveatCheckFailed, CaveatNotRecognizedError, AuthInitError,
+    PermissionDenied, IdentityError, DischargeRequiredError, VerificationError,
+    ThirdPartyInfoNotFound
+)
+from macaroonbakery.identity import (
+    Identity, ACLIdentity, SimpleIdentity, IdentityClient, NoIdentities
+)
+from macaroonbakery.keys import generate_key, PrivateKey, PublicKey
+from macaroonbakery.store import MemoryOpsStore, MemoryKeyStore
+from macaroonbakery.third_party import (
+    ThirdPartyCaveatInfo, ThirdPartyInfo, legacy_namespace
+)
+from macaroonbakery.macaroon import (
+    Macaroon, MacaroonJSONDecoder, MacaroonJSONEncoder, ThirdPartyStore,
+    ThirdPartyLocator, macaroon_version
+)
+from macaroonbakery.discharge import (
+    discharge_all, discharge, local_third_party_caveat, ThirdPartyCaveatChecker
+)
+from macaroonbakery.oven import Oven, canonical_ops
+from macaroonbakery.bakery import Bakery
 
 
-def get_version():
-    '''Return the macaroon bakery version as a string.'''
-    return '.'.join(map(str, VERSION))
+__all__ = [
+    'ACLIdentity',
+    'ACLAuthorizer',
+    'AuthChecker',
+    'AuthInfo',
+    'AuthInitError',
+    'Authorizer',
+    'AuthorizerFunc',
+    'Bakery',
+    'BAKERY_V0',
+    'BAKERY_V1',
+    'BAKERY_V2',
+    'BAKERY_V3',
+    'Bakery',
+    'CaveatNotRecognizedError',
+    'Checker',
+    'ClosedAuthorizer',
+    'DischargeRequiredError',
+    'EVERYONE',
+    'Identity',
+    'IdentityClient',
+    'IdentityError',
+    'LATEST_BAKERY_VERSION',
+    'LOGIN_OP',
+    'Macaroon',
+    'MacaroonJSONDecoder',
+    'MacaroonJSONEncoder',
+    'MemoryKeyStore',
+    'MemoryOpsStore',
+    'NoIdentities',
+    'Op',
+    'Oven',
+    'PermissionDenied',
+    'PrivateKey',
+    'PublicKey',
+    'NoIdentities',
+    'SimpleIdentity',
+    'ThirdPartyCaveatCheckFailed',
+    'ThirdPartyCaveatChecker',
+    'ThirdPartyCaveatInfo',
+    'ThirdPartyInfo',
+    'ThirdPartyInfoNotFound',
+    'ThirdPartyLocator',
+    'ThirdPartyStore',
+    'VERSION',
+    'VerificationError',
+    'canonical_ops',
+    'decode_caveat',
+    'discharge',
+    'discharge_all',
+    'encode_caveat',
+    'encode_uvarint',
+    'generate_key',
+    'legacy_namespace',
+    'local_third_party_caveat',
+    'macaroon_version',
+]
diff --git a/macaroonbakery/authorizer.py b/macaroonbakery/authorizer.py
new file mode 100644
index 0000000..b7128c0
--- /dev/null
+++ b/macaroonbakery/authorizer.py
@@ -0,0 +1,107 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import abc
+
+import macaroonbakery
+
+
+# EVERYONE is recognized by ACLAuthorizer as the name of a
+# group that has everyone in it.
+EVERYONE = 'everyone'
+
+
+class Authorizer(object):
+    ''' Used to check whether a given user is allowed to perform a set of
+    operations.
+    '''
+    __metaclass__ = abc.ABCMeta
+
+    @abc.abstractmethod
+    def authorize(self, ctx, id, ops):
+        ''' Checks whether the given identity (which will be None when there is
+        no authenticated user) is allowed to perform the given operations.
+        It should raise an exception only when the authorization cannot be
+        determined, not when the user has been denied access.
+
+        On success, each element of allowed holds whether the respective
+        element of ops has been allowed, and caveats holds any additional
+        third party caveats that apply.
+        If allowed is shorter then ops, the additional elements are assumed to
+        be False.
+        ctx(AuthContext) is the context of the authorization request.
+        :return: a list of boolean and a list of caveats
+        '''
+        raise NotImplementedError('authorize method must be defined in '
+                                  'subclass')
+
+
+class AuthorizerFunc(Authorizer):
+    ''' Implements a simplified version of Authorizer that operates on a single
+    operation at a time.
+    '''
+    def __init__(self, f):
+        '''
+        :param f: a function that takes an identity that operates on a single
+        operation at a time. Will return if this op is allowed as a boolean and
+        and a list of caveat that holds any additional third party caveats
+        that apply.
+        '''
+        self._f = f
+
+    def authorize(self, ctx, identity, ops):
+        '''Implements Authorizer.authorize by calling f with the given identity
+        for each operation.
+        '''
+        allowed = []
+        caveats = []
+        for op in ops:
+            ok, fcaveats = self._f(ctx, identity, op)
+            allowed.append(ok)
+            if fcaveats is not None:
+                caveats.extend(fcaveats)
+        return allowed, caveats
+
+
+class ACLAuthorizer(Authorizer):
+    ''' ACLAuthorizer is an Authorizer implementation that will check access
+    control list (ACL) membership of users. It uses get_acl to find out
+    the ACLs that apply to the requested operations and will authorize an
+    operation if an ACL contains the group "everyone" or if the identity is
+    an instance of ACLIdentity and its allow method returns True for the ACL.
+    '''
+    def __init__(self, get_acl, allow_public=False):
+        '''
+        :param get_acl get_acl will be called with an auth context and an Op.
+        It should return the ACL that applies (an array of string ids).
+        If an entity cannot be found or the action is not recognised,
+        get_acl should return an empty list but no error.
+        :param allow_public: boolean, If True and an ACL contains "everyone",
+        then authorization will be granted even if there is no logged in user.
+        '''
+        self._allow_public = allow_public
+        self._get_acl = get_acl
+
+    def authorize(self, ctx, identity, ops):
+        '''Implements Authorizer.authorize by calling identity.allow to
+        determine whether the identity is a member of the ACLs associated with
+        the given operations.
+        '''
+        if len(ops) == 0:
+            # Anyone is allowed to do nothing.
+            return [], []
+        allowed = [False] * len(ops)
+        has_allow = isinstance(identity, macaroonbakery.ACLIdentity)
+        for i, op in enumerate(ops):
+            acl = self._get_acl(ctx, op)
+            if has_allow:
+                allowed[i] = identity.allow(ctx, acl)
+            else:
+                allowed[i] = self._allow_public and EVERYONE in acl
+        return allowed, []
+
+
+class ClosedAuthorizer(Authorizer):
+    ''' An Authorizer implementation that will never authorize anything.
+    '''
+    def authorize(self, ctx, id, ops):
+        return [False] * len(ops), []
diff --git a/macaroonbakery/bakery.py b/macaroonbakery/bakery.py
index a3fcf88..1e03191 100644
--- a/macaroonbakery/bakery.py
+++ b/macaroonbakery/bakery.py
@@ -1,16 +1,14 @@
 # Copyright 2017 Canonical Ltd.
 # Licensed under the LGPLv3, see LICENCE file for details.
-
-import base64
 from collections import namedtuple
-import json
 import requests
-from macaroonbakery import utils
 
-import nacl.utils
-from nacl.public import Box
+from macaroonbakery import utils
+from macaroonbakery.discharge import discharge
+from macaroonbakery.checkers import checkers
+from macaroonbakery.oven import Oven
+from macaroonbakery.checker import Checker
 
-from pymacaroons import Macaroon
 
 ERR_INTERACTION_REQUIRED = 'interaction required'
 ERR_DISCHARGE_REQUIRED = 'macaroon discharge required'
@@ -18,11 +16,6 @@ TIME_OUT = 30
 DEFAULT_PROTOCOL_VERSION = {'Bakery-Protocol-Version': '1'}
 MAX_DISCHARGE_RETRIES = 3
 
-BAKERY_V0 = 0
-BAKERY_V1 = 1
-BAKERY_V2 = 2
-BAKERY_V3 = 3
-LATEST_BAKERY_VERSION = BAKERY_V3
 NONCE_LEN = 24
 
 
@@ -65,32 +58,6 @@ def discharge_all(macaroon, visit_page=None, jar=None, key=None):
     return discharges
 
 
-def discharge(key, id, caveat=None, checker=None, locator=None):
-    '''Creates a macaroon to discharge a third party caveat.
-
-    @param key nacl key holds the key to use to decrypt the third party
-    caveat information and to encrypt any additional
-    third party caveats returned by the caveat checker
-    @param id bytes holding the id to give to the discharge macaroon.
-    If caveat is empty, then the id also holds the encrypted third party caveat
-    @param caveat bytes holding the encrypted third party caveat.
-    If this is None, id will be used
-    @param checker used to check the third party caveat,
-    and may also return further caveats to be added to
-    the discharge macaroon. object that will have a function
-    check_third_party_caveat taking a dict of third party caveat info
-    as parameter.
-    @param locator used to retrieve information on third parties
-    referred to by third party caveats returned by the checker. Object that
-    will have a third_party_info function taking a location as a string.
-    @return macaroon with third party caveat discharged.
-    '''
-    if caveat is None:
-        caveat = id
-    cav_info = _decode_caveat(key, caveat)
-    return Macaroon(location='', key=cav_info['RootKey'], identifier=id)
-
-
 class _Client:
     def __init__(self, visit_page, jar):
         self._visit_page = visit_page
@@ -111,7 +78,7 @@ class _Client:
         caveats = macaroon.third_party_caveats()
         for caveat in caveats:
             location = caveat.location
-            b_cav_id = caveat.caveat_id.encode('utf-8')
+            b_cav_id = caveat.caveat_id
             if key is not None and location == 'local':
                 # if tuple is only 2 element otherwise TODO add caveat
                 dm = discharge(key, id=b_cav_id)
@@ -154,39 +121,6 @@ class _Client:
             return _acquire_macaroon_from_wait(info.wait_url)
 
 
-def _decode_caveat(key, caveat):
-    '''Attempts to decode caveat by decrypting the encrypted part using key.
-
-    @param key a nacl key.
-    @param caveat bytes to be decoded.
-    @return a dict of third party caveat info.
-    '''
-    data = base64.b64decode(caveat).decode('utf-8')
-    tpid = json.loads(data)
-    third_party_public_key = nacl.public.PublicKey(
-        base64.b64decode(tpid['ThirdPartyPublicKey']))
-    if key.public_key != third_party_public_key:
-        return 'some error'
-    if tpid.get('FirstPartyPublicKey', None) is None:
-        return 'target service public key not specified'
-    # The encrypted string is base64 encoded in the JSON representation.
-    secret = base64.b64decode(tpid['Id'])
-    first_party_public_key = nacl.public.PublicKey(
-        base64.b64decode(tpid['FirstPartyPublicKey']))
-    box = Box(key,
-              first_party_public_key)
-    c = box.decrypt(secret, base64.b64decode(tpid['Nonce']))
-    record = json.loads(c.decode('utf-8'))
-    return {
-        'Condition': record['Condition'],
-        'FirstPartyPublicKey': first_party_public_key,
-        'ThirdPartyKeyPair': key,
-        'RootKey': base64.b64decode(record['RootKey']),
-        'Caveat': caveat,
-        'MacaroonId': id,
-    }
-
-
 def _extract_macaroon_from_response(response):
     '''Extract the macaroon from a direct successful discharge.
 
@@ -226,12 +160,66 @@ def _extract_urls(response):
     return _Info(visit_url=visit_url, wait_url=wait_url)
 
 
-class ThirdPartyInfo:
-    def __init__(self, version, public_key):
-        '''
-        @param version holds latest the bakery protocol version supported
-        by the discharger.
-        @param public_key holds the public nacl key of the third party.
+class Bakery(object):
+    '''Convenience class that contains both an Oven and a Checker.
+    '''
+    def __init__(self, location=None, locator=None, ops_store=None, key=None,
+                 identity_client=None, checker=None, root_key_store=None,
+                 authorizer=None):
+        '''Returns a new Bakery instance which combines an Oven with a
+        Checker for the convenience of callers that wish to use both
+        together.
+        :param: checker holds the checker used to check first party caveats.
+        If this is None, it will use checkers.Checker(None).
+        :param: root_key_store holds the root key store to use.
+        If you need to use a different root key store for different operations,
+        you'll need to pass a root_key_store_for_ops value to Oven directly.
+        :param: root_key_store If this is None, it will use MemoryKeyStore().
+        Note that that is almost certain insufficient for production services
+        that are spread across multiple instances or that need
+        to persist keys across restarts.
+        :param: locator is used to find out information on third parties when
+        adding third party caveats. If this is None, no non-local third
+        party caveats can be added.
+        :param: key holds the private key of the oven. If this is None,
+        no third party caveats may be added.
+        :param: identity_client holds the identity implementation to use for
+        authentication. If this is None, no authentication will be possible.
+        :param: authorizer is used to check whether an authenticated user is
+        allowed to perform operations. If it is None, it will use
+        a ClosedAuthorizer.
+        The identity parameter passed to authorizer.allow will
+        always have been obtained from a call to
+        IdentityClient.declared_identity.
+        :param: ops_store used to persistently store the association of
+        multi-op entities with their associated operations
+        when oven.macaroon is called with multiple operations.
+        :param: location holds the location to use when creating new macaroons.
         '''
-        self.version = version
-        self.public_key = public_key
+
+        if checker is None:
+            checker = checkers.Checker()
+        root_keystore_for_ops = None
+        if root_key_store is not None:
+            def root_keystore_for_ops(ops):
+                return root_key_store
+
+        oven = Oven(key=key,
+                    location=location,
+                    locator=locator,
+                    namespace=checker.namespace(),
+                    root_keystore_for_ops=root_keystore_for_ops,
+                    ops_store=ops_store)
+        self._oven = oven
+
+        self._checker = Checker(checker=checker, authorizer=authorizer,
+                                identity_client=identity_client,
+                                macaroon_opstore=oven)
+
+    @property
+    def oven(self):
+        return self._oven
+
+    @property
+    def checker(self):
+        return self._checker
diff --git a/macaroonbakery/checker.py b/macaroonbakery/checker.py
new file mode 100644
index 0000000..b73c92f
--- /dev/null
+++ b/macaroonbakery/checker.py
@@ -0,0 +1,409 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from collections import namedtuple
+from threading import Lock
+
+
+import pyrfc3339
+
+import macaroonbakery
+import macaroonbakery.checkers as checkers
+
+
+class Op(namedtuple('Op', 'entity, action')):
+    ''' Op holds an entity and action to be authorized on that entity.
+    entity string holds the name of the entity to be authorized.
+
+    @param entity should not contain spaces and should
+    not start with the prefix "login" or "multi-" (conventionally,
+    entity names will be prefixed with the entity type followed
+    by a hyphen.
+    @param action string holds the action to perform on the entity,
+    such as "read" or "delete". It is up to the service using a checker
+    to define a set of operations and keep them consistent over time.
+    '''
+
+
+# LOGIN_OP represents a login (authentication) operation.
+# A macaroon that is associated with this operation generally
+# carries authentication information with it.
+LOGIN_OP = Op(entity='login', action='login')
+
+
+class Checker(object):
+    '''Checker implements an authentication and authorization checker.
+
+    It uses macaroons as authorization tokens but it is not itself responsible
+    for creating the macaroons
+    See the Oven type (TODO) for one way of doing that.
+    '''
+    def __init__(self, checker=checkers.Checker(),
+                 authorizer=macaroonbakery.ClosedAuthorizer(),
+                 identity_client=None,
+                 macaroon_opstore=None):
+        '''
+        :param checker: a first party checker implementing a
+        :param authorizer (Authorizer): used to check whether an authenticated
+        user is allowed to perform operations.
+        The identity parameter passed to authorizer.allow will always have been
+        obtained from a call to identity_client.declared_identity.
+        :param identity_client (IdentityClient) used for interactions with the
+        external identity service used for authentication.
+        If this is None, no authentication will be possible.
+        :param macaroon_opstore (object with new_macaroon and macaroon_ops
+        method): used to retrieve macaroon root keys and other associated
+        information.
+        '''
+        self._first_party_caveat_checker = checker
+        self._authorizer = authorizer
+        if identity_client is None:
+            identity_client = macaroonbakery.NoIdentities()
+        self._identity_client = identity_client
+        self._macaroon_opstore = macaroon_opstore
+
+    def auth(self, mss):
+        ''' Returns a new AuthChecker instance using the given macaroons to
+        inform authorization decisions.
+        @param mss: a list of macaroon lists.
+        '''
+        return AuthChecker(parent=self,
+                           macaroons=mss)
+
+    def namespace(self):
+        ''' Returns the namespace of the first party checker.
+        '''
+        return self._first_party_caveat_checker.namespace()
+
+
+class AuthChecker(object):
+    '''Authorizes operations with respect to a user's request.
+
+    The identity is authenticated only once, the first time any method
+    of the AuthChecker is called, using the context passed in then.
+
+    To find out any declared identity without requiring a login,
+    use allow(ctx); to require authentication but no additional operations,
+    use allow(ctx, LOGIN_OP).
+    '''
+    def __init__(self, parent, macaroons):
+        '''
+
+        :param parent (Checker): used to check first party caveats.
+        :param macaroons: a list of py macaroons
+        '''
+        self._macaroons = macaroons
+        self._init_errors = []
+        self._executed = False
+        self._identity = None
+        self._identity_caveats = []
+        self.parent = parent
+        self._conditions = None
+        self._mutex = Lock()
+
+    def _init(self, ctx):
+        with self._mutex:
+            if not self._executed:
+                self._init_once(ctx)
+                self._executed = True
+        if self._init_errors is not None and len(self._init_errors) > 0:
+            raise macaroonbakery.AuthInitError(self._init_errors[0])
+
+    def _init_once(self, ctx):
+        self._auth_indexes = {}
+        self._conditions = [None]*len(self._macaroons)
+        for i, ms in enumerate(self._macaroons):
+            try:
+                ops, conditions = self.parent._macaroon_opstore.macaroon_ops(
+                    ms)
+            except macaroonbakery.VerificationError as exc:
+                self._init_errors.append(exc.args[0])
+                continue
+
+            # It's a valid macaroon (in principle - we haven't checked first
+            # party caveats).
+            self._conditions[i] = conditions
+            is_login = False
+            for op in ops:
+                if op == LOGIN_OP:
+                    # Don't associate the macaroon with the login operation
+                    # until we've verified that it is valid below
+                    is_login = True
+                else:
+                    if op not in self._auth_indexes:
+                        self._auth_indexes[op] = []
+                    self._auth_indexes[op].append(i)
+            if not is_login:
+                continue
+            # It's a login macaroon. Check the conditions now -
+            # all calls want to see the same authentication
+            # information so that callers have a consistent idea of
+            # the client's identity.
+            #
+            # If the conditions fail, we won't use the macaroon for
+            # identity, but we can still potentially use it for its
+            # other operations if the conditions succeed for those.
+            declared, err = self._check_conditions(ctx, LOGIN_OP, conditions)
+            if err is not None:
+                self._init_errors.append('cannot authorize login macaroon: ' +
+                                         err)
+                continue
+            if self._identity is not None:
+                # We've already found a login macaroon so ignore this one
+                # for the purposes of identity.
+                continue
+
+            try:
+                identity = self.parent._identity_client.declared_identity(
+                    ctx, declared)
+            except macaroonbakery.IdentityError as exc:
+                self._init_errors.append(
+                    'cannot decode declared identity: {}'.format(exc.args[0]))
+                continue
+            if LOGIN_OP not in self._auth_indexes:
+                self._auth_indexes[LOGIN_OP] = []
+            self._auth_indexes[LOGIN_OP].append(i)
+            self._identity = identity
+
+        if self._identity is None:
+            # No identity yet, so try to get one based on the context.
+            try:
+                identity, cavs = self.parent.\
+                    _identity_client.identity_from_context(ctx)
+            except macaroonbakery.IdentityError:
+                self._init_errors.append('could not determine identity')
+            if cavs is None:
+                cavs = []
+            self._identity, self._identity_caveats = identity, cavs
+        return None
+
+    def allow(self, ctx, ops):
+        ''' Checks that the authorizer's request is authorized to
+        perform all the given operations. Note that allow does not check
+        first party caveats - if there is more than one macaroon that may
+        authorize the request, it will choose the first one that does
+        regardless.
+
+        If all the operations are allowed, an AuthInfo is returned holding
+        details of the decision and any first party caveats that must be
+        checked before actually executing any operation.
+
+        If operations include LOGIN_OP, the request should contain an
+        authentication macaroon proving the client's identity. Once an
+        authentication macaroon is chosen, it will be used for all other
+        authorization requests.
+
+        If an operation was not allowed, an exception will be raised which may
+        be DischargeRequiredError holding the operations that remain to
+        be authorized in order to allow authorization to proceed.
+        :param: ctx AuthContext
+        :param: ops an array of Op
+        :return: an AuthInfo object.
+        '''
+        auth_info, _ = self.allow_any(ctx, ops)
+        return auth_info
+
+    def allow_any(self, ctx, ops):
+        ''' like allow except that it will authorize as many of the
+        operations as possible without requiring any to be authorized. If all
+        the operations succeeded, the array will be nil.
+
+        If any the operations failed, the returned error will be the same
+        that allow would return and each element in the returned slice will
+        hold whether its respective operation was allowed.
+
+        If all the operations succeeded, the returned slice will be None.
+
+        The returned AuthInfo will always be non-None.
+
+        The LOGIN_OP operation is treated specially - it is always required if
+        present in ops.
+        :param: ctx AuthContext
+        :param: ops an array of Op
+        :return: an AuthInfo object and the auth used as an array of int.
+        '''
+        authed, used = self._allow_any(ctx, ops)
+        return self._new_auth_info(used), authed
+
+    def _new_auth_info(self, used):
+        info = AuthInfo(identity=self._identity, macaroons=[])
+        for i, is_used in enumerate(used):
+            if is_used:
+                info.macaroons.append(self._macaroons[i])
+        return info
+
+    def _allow_any(self, ctx, ops):
+        self._init(ctx)
+        used = [False]*len(self._macaroons)
+        authed = [False]*len(ops)
+        num_authed = 0
+        errors = []
+        for i, op in enumerate(ops):
+            for mindex in self._auth_indexes.get(op, []):
+                _, err = self._check_conditions(ctx, op,
+                                                self._conditions[mindex])
+                if err is not None:
+                    errors.append(err)
+                    continue
+                authed[i] = True
+                num_authed += 1
+                used[mindex] = True
+                # Use the first authorized macaroon only.
+                break
+            if op == LOGIN_OP and not authed[i] and self._identity is not None:
+                # Allow LOGIN_OP when there's an authenticated user even
+                # when there's no macaroon that specifically authorizes it.
+                authed[i] = True
+        if self._identity is not None:
+            # We've authenticated as a user, so even if the operations didn't
+            # specifically require it, we add the login macaroon
+            # to the macaroons used.
+            # Note that the LOGIN_OP conditions have already been checked
+            # successfully in initOnceFunc so no need to check again.
+            # Note also that there may not be any macaroons if the
+            # identity client decided on an identity even with no
+            # macaroons.
+            for i in self._auth_indexes.get(LOGIN_OP, []):
+                used[i] = True
+        if num_authed == len(ops):
+            # All operations allowed.
+            return authed, used
+        # There are some unauthorized operations.
+        need = []
+        need_index = [0]*(len(ops)-num_authed)
+        for i, ok in enumerate(authed):
+            if not ok:
+                need_index[len(need)] = i
+                need.append(ops[i])
+
+        # Try to authorize the operations
+        # even if we haven't got an authenticated user.
+        oks, caveats = self.parent._authorizer.authorize(
+            ctx, self._identity, need)
+        still_need = []
+        for i, _ in enumerate(need):
+            if i < len(oks) and oks[i]:
+                authed[need_index[i]] = True
+            else:
+                still_need.append(ops[need_index[i]])
+        if len(still_need) == 0 and len(caveats) == 0:
+            # No more ops need to be authenticated and
+            # no caveats to be discharged.
+            return authed, used
+        if self._identity is None and len(self._identity_caveats) > 0:
+            raise macaroonbakery.DischargeRequiredError(
+                msg='authentication required',
+                ops=[LOGIN_OP],
+                cavs=self._identity_caveats)
+        if caveats is None or len(caveats) == 0:
+            all_errors = []
+            all_errors.extend(self._init_errors)
+            all_errors.extend(errors)
+            err = ''
+            if len(all_errors) > 0:
+                err = all_errors[0]
+            raise macaroonbakery.PermissionDenied(err)
+        raise macaroonbakery.DischargeRequiredError(
+            msg='some operations have extra caveats', ops=ops, cavs=caveats)
+
+    def allow_capability(self, ctx, ops):
+        '''Checks that the user is allowed to perform all the
+        given operations. If not, a discharge error will be raised.
+        If allow_capability succeeds, it returns a list of first party caveat
+        conditions that must be applied to any macaroon granting capability
+        to execute the operations. Those caveat conditions will not
+        include any declarations contained in login macaroons - the
+        caller must be careful not to mint a macaroon associated
+        with the LOGIN_OP operation unless they add the expected
+        declaration caveat too - in general, clients should not create
+        capabilities that grant LOGIN_OP rights.
+
+        The operations must include at least one non-LOGIN_OP operation.
+        '''
+        nops = 0
+        for op in ops:
+            if op != LOGIN_OP:
+                nops += 1
+        if nops == 0:
+            raise ValueError('no non-login operations required in capability')
+
+        _, used = self._allow_any(ctx, ops)
+        squasher = _CaveatSquasher()
+        for i, is_used in enumerate(used):
+            if not is_used:
+                continue
+            for cond in self._conditions[i]:
+                squasher.add(cond)
+        return squasher.final()
+
+    def _check_conditions(self, ctx, op, conds):
+        declared = checkers.infer_declared_from_conditions(
+            conds,
+            self.parent.namespace())
+        ctx = checkers.context_with_operations(ctx, [op.action])
+        ctx = checkers.context_with_declared(ctx, declared)
+        for cond in conds:
+            err = self.parent._first_party_caveat_checker.\
+                check_first_party_caveat(ctx, cond)
+            if err is not None:
+                return None, err
+        return declared, None
+
+
+class AuthInfo(namedtuple('AuthInfo', 'identity macaroons')):
+    '''AuthInfo information about an authorization decision.
+
+    :param: identity: holds information on the authenticated user as
+    returned identity_client. It may be None after a successful
+    authorization if LOGIN_OP access was not required.
+
+    :param: macaroons: holds all the macaroons that were used for the
+    authorization. Macaroons that were invalid or unnecessary are
+    not included.
+    '''
+
+
+class _CaveatSquasher(object):
+    ''' Rationalizes first party caveats created for a capability by:
+        - including only the earliest time-before caveat.
+        - excluding allow and deny caveats (operations are checked by
+        virtue of the operations associated with the macaroon).
+        - removing declared caveats.
+        - removing duplicates.
+    '''
+    def __init__(self, expiry=None, conds=None):
+        self._expiry = expiry
+        if conds is None:
+            conds = []
+        self._conds = conds
+
+    def add(self, cond):
+        if self._add(cond):
+            self._conds.append(cond)
+
+    def _add(self, cond):
+        try:
+            cond, args = checkers.parse_caveat(cond)
+        except ValueError:
+            # Be safe - if we can't parse the caveat, just leave it there.
+            return True
+
+        if cond == checkers.COND_TIME_BEFORE:
+            try:
+                et = pyrfc3339.parse(args)
+            except ValueError:
+                # Again, if it doesn't seem valid, leave it alone.
+                return True
+            if self._expiry is None or et <= self._expiry:
+                self._expiry = et
+            return False
+        elif cond in [checkers.COND_ALLOW,
+                      checkers.COND_DENY, checkers.COND_DECLARED]:
+            return False
+        return True
+
+    def final(self):
+        if self._expiry is not None:
+            self._conds.append(
+                checkers.time_before_caveat(self._expiry).condition)
+        # Make deterministic and eliminate duplicates.
+        return sorted(set(self._conds))
diff --git a/macaroonbakery/checkers.py b/macaroonbakery/checkers.py
deleted file mode 100644
index 8d72eb9..0000000
--- a/macaroonbakery/checkers.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright 2017 Canonical Ltd.
-# Licensed under the LGPLv3, see LICENCE file for details.
-
-import collections
-
-_Caveat = collections.namedtuple('Caveat', 'condition location namespace')
-
-
-class Caveat(_Caveat):
-    '''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)
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',
... 6293 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/py-macaroon-bakery.git



More information about the Python-modules-commits mailing list