[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