async def test_macaroon_auth(event_loop): auth_info, username = agent_auth_info() # Create a bakery client that can do agent authentication. client = httpbakery.Client( key=auth_info.key, interaction_methods=[agent.AgentInteractor(auth_info)], ) async with base.CleanModel(bakery_client=client) as m: async with await m.get_controller() as c: await c.grant_model(username, m.info.uuid, 'admin') async with Model( jujudata=NoAccountsJujuData(m._connector.jujudata), bakery_client=client, ): pass
async def test_macaroon_auth_with_unauthorized_user(event_loop): auth_info, username = agent_auth_info() # Create a bakery client can do agent authentication. client = httpbakery.Client( key=auth_info.key, interaction_methods=[agent.AgentInteractor(auth_info)], ) async with base.CleanModel(bakery_client=client) as m: # Note: no grant of rights to the agent user. try: async with Model( jujudata=NoAccountsJujuData(m._connector.jujudata), bakery_client=client, ): pytest.fail('Should not be able to connect without grant') except JujuAPIError: # We're expecting this because we're using the # wrong user name. pass
async def test_macaroon_auth_with_bad_key(event_loop): auth_info, username = agent_auth_info() # Use a random key rather than the correct key. auth_info = auth_info._replace(key=bakery.generate_key()) # Create a bakery client can do agent authentication. client = httpbakery.Client( key=auth_info.key, interaction_methods=[agent.AgentInteractor(auth_info)], ) async with base.CleanModel(bakery_client=client) as m: async with await m.get_controller() as c: await c.grant_model(username, m.info.uuid, 'admin') try: async with Model( jujudata=NoAccountsJujuData(m._connector.jujudata), bakery_client=client, ): pytest.fail('Should not be able to connect with invalid key') except httpbakery.BakeryException: # We're expecting this because we're using the # wrong key. pass
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': request.url}} cs = SimpleCookie() cookies = request.headers.get('Cookie') if cookies is not None: cs.load(str(cookies)) public_key = None for c in cs: if c == 'agent-login': json_cookie = json.loads( base64.b64decode(cs[c].value).decode('utf-8')) public_key = bakery.PublicKey.deserialize( json_cookie.get('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): 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')
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.serialize_json() } } key = bakery.generate_key() @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( 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( 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')