def test_operations_checker(self): tests = [ ('all allowed', checkers.allow_caveat(['op1', 'op2', 'op4', 'op3' ]), ['op1', 'op3', 'op2'], None), ('none denied', checkers.deny_caveat(['op1', 'op2']), ['op3', 'op4'], None), ('one not allowed', checkers.allow_caveat(['op1', 'op2']), ['op1', 'op3'], 'caveat "allow op1 op2" not satisfied: op3 not allowed'), ('one not denied', checkers.deny_caveat(['op1', 'op2']), ['op4', 'op5', 'op2'], 'caveat "deny op1 op2" not satisfied: op2 not allowed'), ('no operations, allow caveat', checkers.allow_caveat(['op1']), [], 'caveat "allow op1" not satisfied: op1 not allowed'), ('no operations, deny caveat', checkers.deny_caveat(['op1']), [], None), ('no operations, empty allow caveat', checkers.Caveat(condition=checkers.COND_ALLOW), [], 'caveat "allow" not satisfied: no operations allowed'), ] checker = checkers.Checker() for test in tests: print(test[0]) ctx = checkers.context_with_operations(checkers.AuthContext(), test[2]) err = checker.check_first_party_caveat(ctx, test[1].condition) if test[3] is None: self.assertIsNone(err) continue self.assertEqual(err, test[3])
def __call__(self, request): if not request.external_auth_info: return HttpResponseNotFound('Not found') macaroon_bakery = _get_bakery(request) req_headers = request_headers(request) auth_checker = macaroon_bakery.checker.auth( httpbakery.extract_macaroons(req_headers)) try: auth_info = auth_checker.allow( checkers.AuthContext(), [bakery.LOGIN_OP]) except bakery.DischargeRequiredError as err: return _authorization_request( macaroon_bakery, derr=err, req_headers=req_headers) except bakery.VerificationError: return _authorization_request( macaroon_bakery, req_headers=req_headers, auth_endpoint=request.external_auth_info.url) except bakery.PermissionDenied: return HttpResponseForbidden() # a user is always returned since the authentication middleware creates # one if not found user = authenticate(request, identity=auth_info.identity) login( request, user, backend='maasserver.macaroon_auth.MacaroonAuthorizationBackend') return JsonResponse({'id': user.id, 'username': user.username})
def is_authenticated(self, request): if not request.external_auth_info: return False req_headers = request_headers(request) macaroon_bakery = _get_bakery(request) auth_checker = macaroon_bakery.checker.auth( httpbakery.extract_macaroons(req_headers)) try: auth_info = auth_checker.allow(checkers.AuthContext(), [bakery.LOGIN_OP]) except (bakery.DischargeRequiredError, bakery.PermissionDenied): return False # set the user in the request so that it's considered authenticated. If # a user is not found with the username from the identity, it's # created. username = auth_info.identity.id() try: user = User.objects.get(username=username) if user.userprofile.is_local: return False except User.DoesNotExist: user = User(username=username) user.save() if not validate_user_external_auth(user, request.external_auth_info): return False request.user = user return True
def discharge(url, request): qs = parse_qs(request.body) if qs.get('token64') is None: return response(status_code=401, content={ 'Code': httpbakery.ERR_INTERACTION_REQUIRED, 'Message': 'interaction required', 'Info': { 'InteractionMethods': { 'agent': { 'login-url': '/login' }, }, }, }, headers={'Content-Type': 'application/json'}) else: qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, discharge_key, None, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.serialize_json() } }
def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon(version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response( status_code=401, content=content, headers=headers, ) return request.hooks['response'][0](resp)
def __call__(self, request): auth_endpoint = Config.objects.get_config('external_auth_url') if not auth_endpoint: return HttpResponseNotFound('Not found') macaroon_bakery = self._setup_bakery(auth_endpoint, request) req_headers = request_headers(request) auth_checker = macaroon_bakery.checker.auth( httpbakery.extract_macaroons(req_headers)) try: auth_info = auth_checker.allow( checkers.AuthContext(), [bakery.LOGIN_OP]) except bakery.DischargeRequiredError as err: return self._authorization_request( request, req_headers, macaroon_bakery, err) except bakery.PermissionDenied: return HttpResponseForbidden() # a user is always returned since the authentication middleware creates # one if not found user = authenticate(request, identity=auth_info.identity) backend = ( 'maasserver.views.macaroon_auth.MacaroonAuthenticationBackend') login(request, user, backend=backend) return JsonResponse({'id': user.id, 'username': user.username})
def __call__(self, request): if not request.external_auth_info: return HttpResponseNotFound('Not found') macaroon_bakery = _get_bakery(request) req_headers = request_headers(request) auth_checker = macaroon_bakery.checker.auth( httpbakery.extract_macaroons(req_headers)) try: auth_info = auth_checker.allow( checkers.AuthContext(), [bakery.LOGIN_OP]) except bakery.DischargeRequiredError as err: return _authorization_request( macaroon_bakery, derr=err, req_headers=req_headers) except bakery.VerificationError: return _authorization_request( macaroon_bakery, req_headers=req_headers, auth_endpoint=request.external_auth_info.url, auth_domain=request.external_auth_info.domain) except bakery.PermissionDenied: return HttpResponseForbidden() user = authenticate(request, identity=auth_info.identity) if user is None: # macaroon authentication can return None if the user exists but # doesn't have permission to log in return HttpResponseForbidden('User login not allowed') login( request, user, backend='maasserver.macaroon_auth.MacaroonAuthorizationBackend') return JsonResponse( {attr: getattr(user, attr) for attr in ('id', 'username', 'is_superuser')})
def is_authenticated(self, request): if not request.external_auth_info: return False req_headers = request_headers(request) macaroon_bakery = _get_bakery(request) auth_checker = macaroon_bakery.checker.auth( httpbakery.extract_macaroons(req_headers)) try: auth_info = auth_checker.allow( checkers.AuthContext(), [bakery.LOGIN_OP]) except (bakery.DischargeRequiredError, bakery.PermissionDenied): return False # set the user in the request so that it's considered authenticated. If # a user is not found with the username from the identity, it's # created. user, created = User.objects.get_or_create( username=auth_info.identity.id(), defaults={'is_superuser': True}) # Only check the user with IDM again if it wasn't just created if not created and not validate_user_external_auth(user): return False request.user = user return True
def test_authorize_func(self): def f(ctx, identity, op): self.assertEqual(identity.id(), 'bob') if op.entity == 'a': return False, None elif op.entity == 'b': return True, None elif op.entity == 'c': return True, [ checkers.Caveat(location='somewhere', condition='c') ] elif op.entity == 'd': return True, [ checkers.Caveat(location='somewhere', condition='d') ] else: self.fail('unexpected entity: ' + op.Entity) ops = [ macaroonbakery.Op('a', 'x'), macaroonbakery.Op('b', 'x'), macaroonbakery.Op('c', 'x'), macaroonbakery.Op('d', 'x') ] allowed, caveats = macaroonbakery.AuthorizerFunc(f).authorize( checkers.AuthContext(), macaroonbakery.SimpleIdentity('bob'), ops) self.assertEqual(allowed, [False, True, True, True]) self.assertEqual(caveats, [ checkers.Caveat(location='somewhere', condition='c'), checkers.Caveat(location='somewhere', condition='d') ])
def test_context_wired_properly(self): ctx = checkers.AuthContext({'a': 'aval'}) class Visited: in_f = False in_allow = False in_get_acl = False def f(ctx, identity, op): self.assertEqual(ctx.get('a'), 'aval') Visited.in_f = True return False, None macaroonbakery.AuthorizerFunc(f).authorize( ctx, macaroonbakery.SimpleIdentity('bob'), ['op1']) self.assertTrue(Visited.in_f) class TestIdentity(SimplestIdentity, macaroonbakery.ACLIdentity): def allow(other, ctx, acls): self.assertEqual(ctx.get('a'), 'aval') Visited.in_allow = True return False def get_acl(ctx, acl): self.assertEqual(ctx.get('a'), 'aval') Visited.in_get_acl = True return [] macaroonbakery.ACLAuthorizer(allow_public=False, get_acl=get_acl).authorize( ctx, TestIdentity('bob'), ['op1']) self.assertTrue(Visited.in_get_acl) self.assertTrue(Visited.in_allow)
def discharge(url, request): self.assertEqual(url.path, '/discharge') qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return {'status_code': 200, 'content': {'Macaroon': m.to_dict()}}
def verify(self, macaroons): """Verify macaroons and return authenticated user details.""" auth_checker = self._bakery.checker.auth(macaroons) ctx = checkers.AuthContext() # raises DischargeRequiredError if invalid auth or expired macaroon auth_info = auth_checker.allow(ctx, [bakery.LOGIN_OP]) user = auth_info.identity return {'username': user.username(), 'domain': user.domain()}
def test_acl_authorizer(self): ctx = checkers.AuthContext() tests = [ ('no ops, no problem', bakery.ACLAuthorizer(allow_public=True, get_acl=lambda x, y: []), None, [], []), ('identity that does not implement ACLIdentity; ' 'user should be denied except for everyone group', bakery.ACLAuthorizer( allow_public=True, get_acl=lambda ctx, op: [bakery.EVERYONE] if op.entity == 'a' else ['alice'], ), SimplestIdentity('bob'), [bakery.Op(entity='a', action='a'), bakery.Op(entity='b', action='b')], [True, False]), ('identity that does not implement ACLIdentity with user == Id; ' 'user should be denied except for everyone group', bakery.ACLAuthorizer( allow_public=True, get_acl=lambda ctx, op: [bakery.EVERYONE] if op.entity == 'a' else ['bob'], ), SimplestIdentity('bob'), [bakery.Op(entity='a', action='a'), bakery.Op(entity='b', action='b')], [True, False]), ('permission denied for everyone without AllowPublic', bakery.ACLAuthorizer( allow_public=False, get_acl=lambda x, y: [bakery.EVERYONE], ), SimplestIdentity('bob'), [bakery.Op(entity='a', action='a')], [False]), ('permission granted to anyone with no identity with AllowPublic', bakery.ACLAuthorizer( allow_public=True, get_acl=lambda x, y: [bakery.EVERYONE], ), None, [bakery.Op(entity='a', action='a')], [True]) ] for test in tests: allowed, caveats = test[1].authorize(ctx, test[2], test[3]) self.assertEqual(len(caveats), 0) self.assertEqual(allowed, test[4])
def is_authorized(*args, **kwargs): macaroon_bakery = bakery.Bakery( location="ubuntu.com/security", locator=httpbakery.ThirdPartyLocator(), identity_client=IdentityClient(), key=bakery.generate_key(), root_key_store=bakery.MemoryKeyStore( flask.current_app.config["SECRET_KEY"]), ) macaroons = httpbakery.extract_macaroons(flask.request.headers) auth_checker = macaroon_bakery.checker.auth(macaroons) launchpad = Launchpad.login_anonymously("ubuntu.com/security", "production", version="devel") try: auth_info = auth_checker.allow(checkers.AuthContext(), [bakery.LOGIN_OP]) except bakery._error.DischargeRequiredError: macaroon = macaroon_bakery.oven.macaroon( version=bakery.VERSION_2, expiry=datetime.utcnow() + timedelta(days=1), caveats=IDENTITY_CAVEATS, ops=[bakery.LOGIN_OP], ) content, headers = httpbakery.discharge_required_response( macaroon, "/", "cookie-suffix") return content, 401, headers username = auth_info.identity.username() lp_user = launchpad.people(username) authorized = False for team in AUTHORIZED_TEAMS: if lp_user in launchpad.people(team).members: authorized = True break if not authorized: return ( f"{username} is not in any of the authorized teams: " f"{str(AUTHORIZED_TEAMS)}", 401, ) # Validate authentication token return func(*args, **kwargs)
def wait(url, request): class EmptyChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): return [] if InfoStorage.info is None: self.fail('visit url has not been visited') m = bakery.discharge( checkers.AuthContext(), InfoStorage.info.id, InfoStorage.info.caveat, discharge_key, EmptyChecker(), _DischargerLocator(), ) return {'status_code': 200, 'content': {'Macaroon': m.to_dict()}}
def do_GET(self): '''do_GET implements a handler for the HTTP GET method''' ctx = checkers.AuthContext() auth_checker = self._bakery.checker.auth( httpbakery.extract_macaroons(self.headers)) try: auth_checker.allow(ctx, [TEST_OP]) except (bakery.PermissionDenied, bakery.VerificationError) as exc: return self._write_discharge_error(exc) self.send_response(200) self.end_headers() content_len = int(self.headers.get('content-length', 0)) content = 'done' if self.path != '/no-body' and content_len > 0: body = self.rfile.read(content_len) content = content + ' ' + body self.wfile.write(content.encode('utf-8')) return
def discharge(url, request): qs = parse_qs(request.body) if qs.get('caveat64') is not None: content = {q: qs[q][0] for q in qs} class InteractionRequiredError(Exception): def __init__(self, error): self.error = error class CheckerInError(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): InfoStorage.info = info raise InteractionRequiredError( httpbakery.Error( code=httpbakery.ERR_INTERACTION_REQUIRED, version=httpbakery.request_version( request.headers), message='interaction required', info=httpbakery.ErrorInfo( wait_url='http://0.3.2.1/wait?' 'dischargeid=1', visit_url='http://0.3.2.1/visit?' 'dischargeid=1'), ), ) try: httpbakery.discharge(checkers.AuthContext(), content, discharge_key, None, CheckerInError()) except InteractionRequiredError as exc: return response( status_code=401, content={ 'Code': exc.error.code, 'Message': exc.error.message, 'Info': { 'WaitURL': exc.error.info.wait_url, 'VisitURL': exc.error.info.visit_url, }, }, headers={'Content-Type': 'application/json'})
def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} httpbakery.discharge(checkers.AuthContext(), content, d.key, d, ThirdPartyCaveatCheckerF(check))
import macaroonbakery.checkers as checkers class _StoppedClock(object): def __init__(self, t): self.t = t def utcnow(self): return self.t epoch = pytz.utc.localize( datetime(year=1900, month=11, day=17, hour=19, minute=00, second=13)) ages = epoch + timedelta(days=1) test_context = checkers.context_with_clock(checkers.AuthContext(), _StoppedClock(epoch)) def test_checker(): c = checkers.Checker() c.namespace().register('testns', '') c.register('str', 'testns', str_check) c.register('true', 'testns', true_check) return c _str_key = checkers.ContextKey('str_check') def str_context(s):
from ._error import ( ThirdPartyCaveatCheckFailed, CaveatNotRecognizedError, VerificationError, ) from ._codec import decode_caveat from ._macaroon import ( Macaroon, ThirdPartyLocator, ) from ._versions import VERSION_2 from ._third_party import ThirdPartyCaveatInfo import macaroonbakery.checkers as checkers emptyContext = checkers.AuthContext() def discharge_all(m, get_discharge, local_key=None): '''Gathers discharge macaroons for all the third party caveats in m (and any subsequent caveats required by those) using get_discharge to acquire each discharge macaroon. The local_key parameter may optionally hold the key of the client, in which case it will be used to discharge any third party caveats with the special location "local". In this case, the caveat itself must be "true". This can be used be a server to ask a client to prove ownership of the private key. It returns a list of macaroon with m as the first element, followed by all the discharge macaroons. All the discharge macaroons will be bound to the primary macaroon.
def test_checkers(self): tests = [ ('nothing in context, no extra checkers', [ ('something', 'caveat "something" not satisfied: caveat not recognized'), ('', 'cannot parse caveat "": empty caveat'), (' hello', 'cannot parse caveat " hello": caveat starts with' ' space character'), ], None), ('one failed caveat', [ ('t:a aval', None), ('t:b bval', None), ('t:a wrong', 'caveat "t:a wrong" not satisfied: wrong arg'), ], None), ('time from clock', [ (checkers.time_before_caveat(datetime.utcnow() + timedelta(0, 1)).condition, None), (checkers.time_before_caveat(NOW).condition, 'caveat "time-before 2006-01-02T15:04:05.000123Z" ' 'not satisfied: macaroon has expired'), (checkers.time_before_caveat(NOW - timedelta(0, 1)).condition, 'caveat "time-before 2006-01-02T15:04:04.000123Z" ' 'not satisfied: macaroon has expired'), ('time-before bad-date', 'caveat "time-before bad-date" not satisfied: ' 'cannot parse "bad-date" as RFC 3339'), (checkers.time_before_caveat(NOW).condition + " ", 'caveat "time-before 2006-01-02T15:04:05.000123Z " ' 'not satisfied: ' 'cannot parse "2006-01-02T15:04:05.000123Z " as RFC 3339'), ], lambda x: checkers.context_with_clock(ctx, TestClock())), ('real time', [ (checkers.time_before_caveat( datetime(year=2010, month=1, day=1)).condition, 'caveat "time-before 2010-01-01T00:00:00.000000Z" not ' 'satisfied: macaroon has expired'), (checkers.time_before_caveat( datetime(year=3000, month=1, day=1)).condition, None), ], None), ('declared, no entries', [ (checkers.declared_caveat('a', 'aval').condition, 'caveat "declared a aval" not satisfied: got a=null, ' 'expected "aval"'), (checkers.COND_DECLARED, 'caveat "declared" not satisfied: ' 'declared caveat has no value'), ], None), ('declared, some entries', [ (checkers.declared_caveat('a', 'aval').condition, None), (checkers.declared_caveat('b', 'bval').condition, None), (checkers.declared_caveat('spc', ' a b').condition, None), (checkers.declared_caveat('a', 'bval').condition, 'caveat "declared a bval" not satisfied: ' 'got a="aval", expected "bval"'), (checkers.declared_caveat('a', ' aval').condition, 'caveat "declared a aval" not satisfied: ' 'got a="aval", expected " aval"'), (checkers.declared_caveat('spc', 'a b').condition, 'caveat "declared spc a b" not satisfied: ' 'got spc=" a b", expected "a b"'), (checkers.declared_caveat('', 'a b').condition, 'caveat "error invalid caveat \'declared\' key """ ' 'not satisfied: bad caveat'), (checkers.declared_caveat('a b', 'a b').condition, 'caveat "error invalid caveat \'declared\' key "a b"" ' 'not satisfied: bad caveat'), ], lambda x: checkers.context_with_declared(x, { 'a': 'aval', 'b': 'bval', 'spc': ' a b' })), ] checker = checkers.Checker() checker.namespace().register('testns', 't') checker.register('a', 'testns', arg_checker(self, 't:a', 'aval')) checker.register('b', 'testns', arg_checker(self, 't:b', 'bval')) ctx = checkers.AuthContext() for test in tests: print(test[0]) if test[2] is not None: ctx1 = test[2](ctx) else: ctx1 = ctx for check in test[1]: err = checker.check_first_party_caveat(ctx1, check[0]) if check[1] is not None: self.assertEqual(err, check[1]) else: self.assertIsNone(err)