def agent_visit(url, request): if request.method != "POST": raise Exception('unexpected method') log.info('agent_visit url {}'.format(url)) body = json.loads(request.body.decode('utf-8')) if body['username'] != 'test-user': raise Exception('unexpected username in body {!r}'.format( request.body)) public_key = bakery.PublicKey.deserialize(body['public_key']) ms = httpbakery.extract_macaroons(request.headers) if len(ms) == 0: b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( public_key, version=httpbakery.request_version( request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) return {'status_code': 200, 'content': {'agent_login': True}}
def _setup_bakery(self, auth_endpoint, request): return bakery.Bakery( key=_get_macaroon_oven_key(), root_key_store=KeyStore(MACAROON_LIFESPAN), location=request.build_absolute_uri('/'), locator=httpbakery.ThirdPartyLocator( allow_insecure=not auth_endpoint.startswith('https:')), identity_client=IDClient(auth_endpoint), authorizer=bakery.ACLAuthorizer( get_acl=lambda ctx, op: [bakery.EVERYONE]))
def login(url, request): b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( auth_info.key.public_key, version=httpbakery.request_version(request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) return {'status_code': 200, 'content': {'macaroon': m.to_dict()}}
def _get_bakery(request): auth_endpoint = request.external_auth_info.url auth_domain = request.external_auth_info.domain return bakery.Bakery( key=_get_macaroon_oven_key(), root_key_store=KeyStore(MACAROON_LIFESPAN), location=request.build_absolute_uri("/"), locator=httpbakery.ThirdPartyLocator( allow_insecure=not auth_endpoint.startswith("https:")), identity_client=_IDClient(auth_endpoint, auth_domain=auth_domain), authorizer=bakery.ACLAuthorizer( get_acl=lambda ctx, op: [bakery.EVERYONE]), )
def test_version1_macaroon_id(self): # In the version 1 bakery, macaroon ids were hex-encoded with a # hyphenated UUID suffix. root_key_store = bakery.MemoryKeyStore() b = bakery.Bakery( root_key_store=root_key_store, identity_client=common.OneIdentity(), ) key, id = root_key_store.root_key() root_key_store.get(id) m = Macaroon(key=key, version=MACAROON_V1, location='', identifier=id + b'-deadl00f') b.checker.auth([[m]]).allow(common.test_context, [bakery.LOGIN_OP])
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 __init__(self, locator=None): locator = httpbakery.ThirdPartyLocator() # generate a new keypair for encrypting third party caveats # it's safe to use a new keypair every time the server starts # as it's used only for encrypting the third party caveats # for sending them to be discharged. The private key doesn't need # to survive across restarts. key = bakery.generate_key() location = 'localhost:8000' root_key = 'private-key' self._bakery = bakery.Bakery( location=location, locator=locator, identity_client=IdentityClient(), key=key, root_key_store=bakery.MemoryKeyStore(root_key))
def test_third_party_discharge_macaroon_wrong_root_key_and_third_party_caveat( self): root_keys = bakery.MemoryKeyStore() ts = bakery.Bakery( key=bakery.generate_key(), checker=common.test_checker(), root_key_store=root_keys, identity_client=common.OneIdentity(), ) locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) # ts creates a macaroon with a third party caveat addressed to bs. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) ts_macaroon.add_caveat( checkers.Caveat(location='bs-loc', condition='true'), ts.oven.key, locator, ) def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('true'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) # The authorization should succeed at first. ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) # Corrupt the root key and try again. # We should get a DischargeRequiredError because the verification has failed. root_keys._key = os.urandom(24) with self.assertRaises(bakery.PermissionDenied) as err: ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) self.assertEqual( str(err.exception), 'verification failed: Decryption failed. Ciphertext failed verification' )
def login(url, request): qs = parse_qs(urlparse(request.url).query) self.assertEqual(request.method, 'GET') self.assertEqual(qs, { 'username': ['test-user'], 'public-key': [PUBLIC_KEY] }) b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( PUBLIC_KEY, version=httpbakery.request_version(request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) return {'status_code': 200, 'content': {'macaroon': m.to_dict()}}
def new_bakery(location, locator=None): # Returns a new Bakery instance using a new # key pair, and registers the key with the given locator if provided. # # It uses test_checker to check first party caveats. key = bakery.generate_key() if locator is not None: locator.add_info( location, bakery.ThirdPartyInfo(public_key=key.public_key, version=bakery.LATEST_VERSION)) return bakery.Bakery( key=key, checker=test_checker(), location=location, identity_client=OneIdentity(), locator=locator, )
def new_bakery(location, locator, checker): '''Return a new bakery instance. @param location Location of the bakery {str}. @param locator Locator for third parties {ThirdPartyLocator or None} @param checker Caveat checker {FirstPartyCaveatChecker or None} @return {Bakery} ''' if checker is None: c = checkers.Checker() c.namespace().register('testns', '') c.register('is', 'testns', check_is_something) checker = c key = bakery.generate_key() return bakery.Bakery( location=location, locator=locator, key=key, checker=checker, )
def test_agent_login(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') 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) @urlmatch(path='.*/discharge') 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.to_dict() } } auth_info = agent.load_auth_info(self.agent_filename) @urlmatch(path='.*/login') def login(url, request): b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( auth_info.key.public_key, version=httpbakery.request_version(request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) return {'status_code': 200, 'content': {'macaroon': m.to_dict()}} with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(login): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor(auth_info), ]) resp = requests.get('http://0.1.2.3/here', cookies=client.cookies, auth=client.auth()) self.assertEquals(resp.content, b'done')
def test_agent_legacy(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') 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) class InfoStorage: info = None @urlmatch(path='.*/discharge') 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'}) key = bakery.generate_key() @urlmatch(path='.*/visit') def visit(url, request): if request.headers.get('Accept') == 'application/json': return { 'status_code': 200, 'content': { 'agent': '/agent-visit', } } raise Exception('unexpected call to visit without Accept header') @urlmatch(path='.*/agent-visit') def agent_visit(url, request): if request.method != "POST": raise Exception('unexpected method') log.info('agent_visit url {}'.format(url)) body = json.loads(request.body.decode('utf-8')) if body['username'] != 'test-user': raise Exception('unexpected username in body {!r}'.format( request.body)) public_key = bakery.PublicKey.deserialize(body['public_key']) ms = httpbakery.extract_macaroons(request.headers) if len(ms) == 0: b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( public_key, version=httpbakery.request_version( request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) return {'status_code': 200, 'content': {'agent_login': True}} @urlmatch(path='.*/wait$') 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()}} with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(visit), \ HTTMock(wait), \ HTTMock(agent_visit): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor( agent.AuthInfo( key=key, agents=[ agent.Agent(username='******', url=u'http://0.3.2.1') ], ), ), ]) resp = requests.get( 'http://0.1.2.3/here', cookies=client.cookies, auth=client.auth(), ) self.assertEquals(resp.content, b'done')