diff options
author | Colin Watson <cjwatson@debian.org> | 2017-12-12 15:20:49 +0000 |
---|---|---|
committer | Colin Watson <cjwatson@debian.org> | 2017-12-12 15:20:49 +0000 |
commit | 9e4403035a9953c99117083e6373ae3c441a76b5 (patch) | |
tree | d91b137df6767bfb8cb72de6b9fd21efb0c3dee4 /macaroonbakery/tests | |
parent | 949b7072cabce0daed6c94993ad44c8ea8648dbd (diff) |
Import py-macaroon-bakery_1.1.0.orig.tar.gz
Diffstat (limited to 'macaroonbakery/tests')
-rw-r--r-- | macaroonbakery/tests/common.py | 5 | ||||
-rw-r--r-- | macaroonbakery/tests/test_agent.py | 171 | ||||
-rw-r--r-- | macaroonbakery/tests/test_authorizer.py | 2 | ||||
-rw-r--r-- | macaroonbakery/tests/test_bakery.py | 87 | ||||
-rw-r--r-- | macaroonbakery/tests/test_checker.py | 34 | ||||
-rw-r--r-- | macaroonbakery/tests/test_checkers.py | 7 | ||||
-rw-r--r-- | macaroonbakery/tests/test_client.py | 130 | ||||
-rw-r--r-- | macaroonbakery/tests/test_codec.py | 7 | ||||
-rw-r--r-- | macaroonbakery/tests/test_discharge.py | 5 | ||||
-rw-r--r-- | macaroonbakery/tests/test_discharge_all.py | 5 | ||||
-rw-r--r-- | macaroonbakery/tests/test_keyring.py | 16 | ||||
-rw-r--r-- | macaroonbakery/tests/test_macaroon.py | 12 | ||||
-rw-r--r-- | macaroonbakery/tests/test_oven.py | 8 | ||||
-rw-r--r-- | macaroonbakery/tests/test_store.py | 2 | ||||
-rw-r--r-- | macaroonbakery/tests/test_time.py | 19 | ||||
-rw-r--r-- | macaroonbakery/tests/test_utils.py | 74 |
16 files changed, 376 insertions, 208 deletions
diff --git a/macaroonbakery/tests/common.py b/macaroonbakery/tests/common.py index f238dfd..cfbfc52 100644 --- a/macaroonbakery/tests/common.py +++ b/macaroonbakery/tests/common.py @@ -2,10 +2,9 @@ # Licensed under the LGPLv3, see LICENCE file for details. from datetime import datetime, timedelta -import pytz - -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers +import pytz class _StoppedClock(object): diff --git a/macaroonbakery/tests/test_agent.py b/macaroonbakery/tests/test_agent.py index 67f5b84..3b38337 100644 --- a/macaroonbakery/tests/test_agent.py +++ b/macaroonbakery/tests/test_agent.py @@ -1,27 +1,22 @@ # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. -import base64 -from datetime import datetime, timedelta import json +import logging import os import tempfile +from datetime import datetime, timedelta from unittest import TestCase -import nacl.encoding -import requests.cookies -import six -from six.moves.urllib.parse import parse_qs -from six.moves.http_cookies import SimpleCookie -from httmock import ( - HTTMock, - urlmatch, - response -) - -import macaroonbakery as bakery -import macaroonbakery.httpbakery as httpbakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers +import macaroonbakery.httpbakery as httpbakery import macaroonbakery.httpbakery.agent as agent +import requests.cookies + +from httmock import HTTMock, response, urlmatch +from six.moves.urllib.parse import parse_qs + +log = logging.getLogger(__name__) class TestAgents(TestCase): @@ -44,73 +39,31 @@ class TestAgents(TestCase): os.remove(self.bad_key_agent_filename) os.remove(self.no_username_agent_filename) - def test_load_agents(self): - cookies, key = agent.load_agent_file(self.agent_filename) - self.assertEqual(key.encode(nacl.encoding.Base64Encoder), - b'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=') - self.assertEqual( - key.public_key.encode(nacl.encoding.Base64Encoder), - b'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=') - - value = cookies.get('agent-login', domain='1.example.com') - jv = base64.b64decode(value) - if six.PY3: - jv = jv.decode('utf-8') - data = json.loads(jv) - self.assertEqual(data['username'], 'user-1') - self.assertEqual(data['public_key'], - 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=') - - value = cookies.get('agent-login', domain='2.example.com', - path='/discharger') - jv = base64.b64decode(value) - if six.PY3: - jv = jv.decode('utf-8') - data = json.loads(jv) - self.assertEqual(data['username'], 'user-2') - self.assertEqual(data['public_key'], - 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=') - - def test_load_agents_into_cookies(self): - cookies = requests.cookies.RequestsCookieJar() - c1, key = agent.load_agent_file( - self.agent_filename, - cookies=cookies, - ) - self.assertEqual(c1, cookies) - self.assertEqual( - key.encode(nacl.encoding.Base64Encoder), - b'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=', - ) - self.assertEqual( - key.public_key.encode(nacl.encoding.Base64Encoder), - b'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=', - ) - - value = cookies.get('agent-login', domain='1.example.com') - jv = base64.b64decode(value) - if six.PY3: - jv = jv.decode('utf-8') - data = json.loads(jv) - self.assertEqual(data['username'], 'user-1') - self.assertEqual(data['public_key'], 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=') - - value = cookies.get('agent-login', domain='2.example.com', - path='/discharger') - jv = base64.b64decode(value) - if six.PY3: - jv = jv.decode('utf-8') - data = json.loads(jv) - self.assertEqual(data['username'], 'user-2') - self.assertEqual(data['public_key'], 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=') - - def test_load_agents_with_bad_key(self): + def test_load_auth_info(self): + auth_info = agent.load_auth_info(self.agent_filename) + self.assertEqual(str(auth_info.key), 'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=') + self.assertEqual(str(auth_info.key.public_key), 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=') + self.assertEqual(auth_info.agents, [ + agent.Agent(url='https://1.example.com/', username='user-1'), + agent.Agent(url='https://2.example.com/discharger', username='user-2'), + agent.Agent(url='http://0.3.2.1', username='test-user'), + ]) + + def test_invalid_agent_json(self): + with self.assertRaises(agent.AgentFileFormatError): + agent.read_auth_info('}') + + def test_invalid_read_auth_info_arg(self): with self.assertRaises(agent.AgentFileFormatError): - agent.load_agent_file(self.bad_key_agent_filename) + agent.read_auth_info(0) - def test_load_agents_with_no_username(self): + def test_load_auth_info_with_bad_key(self): with self.assertRaises(agent.AgentFileFormatError): - agent.load_agent_file(self.no_username_agent_filename) + agent.load_auth_info(self.bad_key_agent_filename) + + def test_load_auth_info_with_no_username(self): + with self.assertRaises(agent.AgentFileFormatError): + agent.load_auth_info(self.no_username_agent_filename) def test_agent_login(self): discharge_key = bakery.generate_key() @@ -138,7 +91,8 @@ class TestAgents(TestCase): content='done') except bakery.PermissionDenied: caveats = [ - checkers.Caveat(location='http://0.3.2.1', condition='is-ok') + checkers.Caveat(location='http://0.3.2.1', + condition='is-ok') ] m = server_bakery.oven.macaroon( version=bakery.LATEST_VERSION, @@ -177,11 +131,11 @@ class TestAgents(TestCase): return { 'status_code': 200, 'content': { - 'Macaroon': m.serialize_json() + 'Macaroon': m.to_dict() } } - key = bakery.generate_key() + auth_info = agent.load_auth_info(self.agent_filename) @urlmatch(path='.*/login') def login(url, request): @@ -190,7 +144,7 @@ class TestAgents(TestCase): version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[bakery.local_third_party_caveat( - key.public_key, + auth_info.key.public_key, version=httpbakery.request_version(request.headers))], ops=[bakery.Op(entity='agent', action='login')]) return { @@ -204,17 +158,7 @@ class TestAgents(TestCase): HTTMock(discharge), \ HTTMock(login): client = httpbakery.Client(interaction_methods=[ - agent.AgentInteractor( - agent.AuthInfo( - key=key, - agents=[ - agent.Agent( - username='test-user', - url=u'http://0.3.2.1' - ) - ], - ), - ), + agent.AgentInteractor(auth_info), ]) resp = requests.get( 'http://0.1.2.3/here', @@ -315,25 +259,26 @@ class TestAgents(TestCase): key = bakery.generate_key() - @urlmatch(path='.*/visit?$') + @urlmatch(path='.*/visit') def visit(url, request): if request.headers.get('Accept') == 'application/json': return { 'status_code': 200, 'content': { - 'agent': request.url + 'agent': '/agent-visit', } } - 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')) + 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) @@ -356,11 +301,11 @@ class TestAgents(TestCase): return { 'status_code': 200, 'content': { - 'agent-login': True + 'agent_login': True } } - @urlmatch(path='.*/wait?$') + @urlmatch(path='.*/wait$') def wait(url, request): class EmptyChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): @@ -385,12 +330,14 @@ class TestAgents(TestCase): with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(visit), \ - HTTMock(wait): + HTTMock(wait), \ + HTTMock(agent_visit): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor( agent.AuthInfo( key=key, - agents=[agent.Agent(username='test-user', url=u'http://0.3.2.1')], + agents=[agent.Agent(username='test-user', + url=u'http://0.3.2.1')], ), ), ]) @@ -414,11 +361,13 @@ agent_file = ''' }, { "url": "https://2.example.com/discharger", "username": "user-2" + }, { + "url": "http://0.3.2.1", + "username": "test-user" }] } ''' - bad_key_agent_file = ''' { "key": { diff --git a/macaroonbakery/tests/test_authorizer.py b/macaroonbakery/tests/test_authorizer.py index f90d2b5..d5539b7 100644 --- a/macaroonbakery/tests/test_authorizer.py +++ b/macaroonbakery/tests/test_authorizer.py @@ -2,7 +2,7 @@ # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers diff --git a/macaroonbakery/tests/test_bakery.py b/macaroonbakery/tests/test_bakery.py index 5a13cff..a6c3e58 100644 --- a/macaroonbakery/tests/test_bakery.py +++ b/macaroonbakery/tests/test_bakery.py @@ -2,19 +2,11 @@ # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase +import macaroonbakery.httpbakery as httpbakery import requests +from mock import patch -from mock import ( - patch, -) - -from httmock import ( - HTTMock, - urlmatch, - response -) - -import macaroonbakery.httpbakery as httpbakery +from httmock import HTTMock, response, urlmatch ID_PATH = 'http://example.com/someprotecteurl' @@ -29,7 +21,7 @@ json_macaroon = { }, { u'cid': u'allow read-no-terms write' }, { - u'cid': u'time-before 2016-07-19T14:29:14.312669464Z' + u'cid': u'time-before 2158-07-19T14:29:14.312669464Z' }], u'location': u'charmstore', u'signature': u'52d17cb11f5c84d58441bc0ffd7cc396' @@ -41,7 +33,7 @@ discharge_token = [{ u'caveats': [{ u'cid': u'declared username someone' }, { - u'cid': u'time-before 2016-08-15T15:55:52.428319076Z' + u'cid': u'time-before 2158-08-15T15:55:52.428319076Z' }, { u'cid': u'origin ' }], @@ -57,7 +49,7 @@ discharged_macaroon = { }, { u'cid': u'declared username someone' }, { - u'cid': u'time-before 2016-07-19T15:55:52.432439055Z' + u'cid': u'time-before 2158-07-19T15:55:52.432439055Z' }], u'location': u'', u'signature': u'3513db5503ab17f9576760cd28' @@ -167,6 +159,17 @@ def wait_after_401(url, request): } +@urlmatch(path='.*/wait') +def wait_on_error(url, request): + return { + 'status_code': 500, + 'content': { + 'DischargeToken': discharge_token, + 'Macaroon': discharged_macaroon + } + } + + class TestBakery(TestCase): def assert_cookie_security(self, cookies, name, secure): @@ -185,12 +188,14 @@ class TestBakery(TestCase): auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys() - self.assert_cookie_security(client.cookies, 'macaroon-test', secure=False) + self.assert_cookie_security(client.cookies, 'macaroon-test', + secure=False) @patch('webbrowser.open') def test_407_then_401_on_discharge(self, mock_open): client = httpbakery.Client() - with HTTMock(first_407_then_200), HTTMock(discharge_401), HTTMock(wait_after_401): + with HTTMock(first_407_then_200), HTTMock(discharge_401), \ + HTTMock(wait_after_401): resp = requests.get( ID_PATH, cookies=client.cookies, @@ -200,6 +205,53 @@ class TestBakery(TestCase): mock_open.assert_called_once_with(u'http://example.com/visit', new=1) assert 'macaroon-test' in client.cookies.keys() + @patch('webbrowser.open') + def test_407_then_error_on_wait(self, mock_open): + client = httpbakery.Client() + with HTTMock(first_407_then_200), HTTMock(discharge_401),\ + HTTMock(wait_on_error): + with self.assertRaises(httpbakery.InteractionError) as exc: + requests.get( + ID_PATH, + cookies=client.cookies, + auth=client.auth(), + ) + self.assertEqual(str(exc.exception), + 'cannot start interactive session: cannot get ' + 'http://example.com/wait') + mock_open.assert_called_once_with(u'http://example.com/visit', new=1) + + def test_407_then_no_interaction_methods(self): + client = httpbakery.Client(interaction_methods=[]) + with HTTMock(first_407_then_200), HTTMock(discharge_401): + with self.assertRaises(httpbakery.InteractionError) as exc: + requests.get( + ID_PATH, + cookies=client.cookies, + auth=client.auth(), + ) + self.assertEqual(str(exc.exception), + 'cannot start interactive session: interaction ' + 'required but not possible') + + def test_407_then_unknown_interaction_methods(self): + class UnknownInteractor(httpbakery.Interactor): + def kind(self): + return 'unknown' + client = httpbakery.Client(interaction_methods=[UnknownInteractor()]) + with HTTMock(first_407_then_200), HTTMock(discharge_401): + with self.assertRaises(httpbakery.InteractionError) as exc: + requests.get( + ID_PATH, + cookies=client.cookies, + auth=client.auth(), + ) + self.assertEqual( + str(exc.exception), + 'cannot start interactive session: no methods supported; ' + 'supported [unknown]; provided [interactive]' + ) + def test_cookie_with_port(self): client = httpbakery.Client() with HTTMock(first_407_then_200_with_port): @@ -219,4 +271,5 @@ class TestBakery(TestCase): auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys() - self.assert_cookie_security(client.cookies, 'macaroon-test', secure=True) + self.assert_cookie_security(client.cookies, 'macaroon-test', + secure=True) diff --git a/macaroonbakery/tests/test_checker.py b/macaroonbakery/tests/test_checker.py index 643c756..6b61768 100644 --- a/macaroonbakery/tests/test_checker.py +++ b/macaroonbakery/tests/test_checker.py @@ -1,17 +1,16 @@ # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 -from collections import namedtuple import json -from unittest import TestCase +from collections import namedtuple from datetime import timedelta +from unittest import TestCase -from pymacaroons.verifier import Verifier, FirstPartyCaveatVerifierDelegate -import pymacaroons - -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers -from macaroonbakery.tests.common import test_context, epoch, test_checker +import pymacaroons +from macaroonbakery.tests.common import epoch, test_checker, test_context +from pymacaroons.verifier import FirstPartyCaveatVerifierDelegate, Verifier class TestChecker(TestCase): @@ -53,7 +52,8 @@ class TestChecker(TestCase): client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') - auth_info = client.do(ctx, ts, [bakery.Op(entity='something', action='read')]) + auth_info = client.do(ctx, ts, [bakery.Op(entity='something', + action='read')]) self.assertEqual(self._discharges, [_DischargeRecord(location='ids', user='bob')]) self.assertIsNotNone(auth_info) @@ -98,7 +98,8 @@ class TestChecker(TestCase): self.assertIsNotNone(auth_info) self.assertIsNone(auth_info.identity) self.assertEqual(len(auth_info.macaroons), 1) - self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, m[0].identifier_bytes) + self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, + m[0].identifier_bytes) def test_capability_multiple_entities(self): locator = _DischargerLocator() @@ -168,8 +169,10 @@ class TestChecker(TestCase): self.assertIsNotNone(auth_info) self.assertIsNone(auth_info.identity) self.assertEqual(len(auth_info.macaroons), 2) - self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, m1[0].identifier_bytes) - self.assertEqual(auth_info.macaroons[1][0].identifier_bytes, m2[0].identifier_bytes) + self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, + m1[0].identifier_bytes) + self.assertEqual(auth_info.macaroons[1][0].identifier_bytes, + m2[0].identifier_bytes) def test_combine_capabilities(self): locator = _DischargerLocator() @@ -560,8 +563,10 @@ class TestChecker(TestCase): # Try them the other way around and we should authenticate as alice. client3 = _Client(locator) - client3.add_macaroon(ts, '1.alice', client2._macaroons[ts.name()]['authn']) - client3.add_macaroon(ts, '2.bob', client1._macaroons[ts.name()]['authn']) + client3.add_macaroon(ts, '1.alice', + client2._macaroons[ts.name()]['authn']) + client3.add_macaroon(ts, '2.bob', + client1._macaroons[ts.name()]['authn']) auth_info = client3.do(test_context, ts, [bakery.LOGIN_OP]) self.assertEqual(auth_info.identity.id(), 'alice') @@ -890,7 +895,8 @@ class _BasicAuthIdService(bakery.IdentityClient): return bakery.SimpleIdentity(user), None def declared_identity(self, ctx, declared): - raise bakery.IdentityError('no identity declarations in basic auth id service') + raise bakery.IdentityError('no identity declarations in basic auth' + ' id service') _BASIC_AUTH_KEY = checkers.ContextKey('user-key') diff --git a/macaroonbakery/tests/test_checkers.py b/macaroonbakery/tests/test_checkers.py index f552fa4..28da06e 100644 --- a/macaroonbakery/tests/test_checkers.py +++ b/macaroonbakery/tests/test_checkers.py @@ -3,11 +3,10 @@ from datetime import datetime, timedelta from unittest import TestCase -import six -import pytz -from pymacaroons import Macaroon, MACAROON_V2 - import macaroonbakery.checkers as checkers +import pytz +import six +from pymacaroons import MACAROON_V2, Macaroon # A frozen time for the tests. NOW = datetime( diff --git a/macaroonbakery/tests/test_client.py b/macaroonbakery/tests/test_client.py index e1a4009..ab20c3b 100644 --- a/macaroonbakery/tests/test_client.py +++ b/macaroonbakery/tests/test_client.py @@ -3,23 +3,24 @@ import base64 import datetime import json -from unittest import TestCase -try: - from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler -except ImportError: - from http.server import HTTPServer, BaseHTTPRequestHandler import threading +from unittest import TestCase -from httmock import ( - HTTMock, - urlmatch -) +import macaroonbakery.bakery as bakery +import macaroonbakery.checkers as checkers +import macaroonbakery.httpbakery as httpbakery +import pymacaroons import requests +import macaroonbakery._utils as utils + +from httmock import HTTMock, urlmatch from six.moves.urllib.parse import parse_qs +from six.moves.urllib.request import Request -import macaroonbakery as bakery -import macaroonbakery.httpbakery as httpbakery -import macaroonbakery.checkers as checkers +try: + from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +except ImportError: + from http.server import HTTPServer, BaseHTTPRequestHandler AGES = datetime.datetime.utcnow() + datetime.timedelta(days=1) TEST_OP = bakery.Op(entity='test', action='test') @@ -30,7 +31,7 @@ class TestClient(TestCase): b = new_bakery('loc', None, None) def handler(*args): - GetHandler(b, None, None, None, None, *args) + GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) @@ -58,7 +59,7 @@ class TestClient(TestCase): b = new_bakery('loc', None, None) def handler(*args): - GetHandler(b, None, None, None, None, *args) + GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) @@ -81,7 +82,7 @@ class TestClient(TestCase): finally: httpd.shutdown() - def test_repeated_request_with_body(self): + def test_expiry_cookie_is_set(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() @@ -100,7 +101,8 @@ class TestClient(TestCase): def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} - m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) + m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, + alwaysOK3rd) return { 'status_code': 200, 'content': { @@ -108,8 +110,10 @@ class TestClient(TestCase): } } + ages = datetime.datetime.utcnow() + datetime.timedelta(days=1) + def handler(*args): - GetHandler(b, 'http://1.2.3.4', None, None, None, *args) + GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) @@ -122,10 +126,64 @@ class TestClient(TestCase): cookies=client.cookies, auth=client.auth()) resp.raise_for_status() + m = bakery.Macaroon.from_dict(json.loads( + base64.b64decode(client.cookies.get('macaroon-test')).decode('utf-8'))[0]) + t = checkers.macaroons_expiry_time( + checkers.Namespace(), [m.macaroon]) + self.assertEquals(ages, t) self.assertEquals(resp.text, 'done') finally: httpd.shutdown() + def test_expiry_cookie_set_in_past(self): + class _DischargerLocator(bakery.ThirdPartyLocator): + def __init__(self): + self.key = bakery.generate_key() + + def third_party_info(self, loc): + if loc == 'http://1.2.3.4': + return bakery.ThirdPartyInfo( + public_key=self.key.public_key, + version=bakery.LATEST_VERSION, + ) + + d = _DischargerLocator() + b = new_bakery('loc', d, None) + + @urlmatch(path='.*/discharge') + def discharge(url, request): + 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() + } + } + + ages = datetime.datetime.utcnow() - datetime.timedelta(days=1) + + def handler(*args): + GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args) + try: + httpd = HTTPServer(('', 0), handler) + thread = threading.Thread(target=httpd.serve_forever) + thread.start() + client = httpbakery.Client() + with HTTMock(discharge): + with self.assertRaises(httpbakery.BakeryException) as ctx: + requests.get( + url='http://' + httpd.server_address[0] + ':' + + str(httpd.server_address[1]), + cookies=client.cookies, + auth=client.auth()) + self.assertEqual(ctx.exception.args[0], + 'too many (3) discharge requests') + finally: + httpd.shutdown() + def test_too_many_discharge(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): @@ -155,7 +213,7 @@ class TestClient(TestCase): } def handler(*args): - GetHandler(b, 'http://1.2.3.4', None, None, None, *args) + GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) @@ -199,7 +257,7 @@ class TestClient(TestCase): ThirdPartyCaveatCheckerF(check)) def handler(*args): - GetHandler(b, 'http://1.2.3.4', None, None, None, *args) + GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) @@ -244,7 +302,7 @@ class TestClient(TestCase): } def handler(*args): - GetHandler(b, 'http://1.2.3.4', None, None, None, *args) + GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) @@ -273,11 +331,34 @@ class TestClient(TestCase): finally: httpd.shutdown() + def test_extract_macaroons_from_request(self): + def encode_macaroon(m): + macaroons = '[' + utils.macaroon_to_json_string(m) + ']' + return base64.urlsafe_b64encode(utils.to_bytes(macaroons)).decode('ascii') + + req = Request('http://example.com') + m1 = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2, identifier='one') + req.add_header('Macaroons', encode_macaroon(m1)) + m2 = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2, identifier='two') + jar = requests.cookies.RequestsCookieJar() + jar.set_cookie(utils.cookie( + name='macaroon-auth', + value=encode_macaroon(m2), + url='http://example.com', + )) + jar.add_cookie_header(req) + + macaroons = httpbakery.extract_macaroons(req) + self.assertEquals(len(macaroons), 2) + macaroons.sort(key=lambda ms: ms[0].identifier) + self.assertEquals(macaroons[0][0].identifier, m1.identifier) + self.assertEquals(macaroons[1][0].identifier, m2.identifier) + class GetHandler(BaseHTTPRequestHandler): '''A mock HTTP server that serves a GET request''' def __init__(self, bakery, auth_location, mutate_error, - caveats, version, *args): + caveats, version, expiry, *args): ''' @param bakery used to check incoming requests and macaroons for discharge-required errors. @@ -288,14 +369,17 @@ class GetHandler(BaseHTTPRequestHandler): discharge-required error before responding to the client. @param caveats called to get caveats to add to the returned macaroon. - @param holds the version of the bakery that the + @param version holds the version of the bakery that the server will purport to serve. + @param expiry holds the expiry for the macaroon that will be created + in _write_discharge_error ''' self._bakery = bakery self._auth_location = auth_location self._mutate_error = mutate_error self._caveats = caveats self._server_version = version + self._expiry = expiry BaseHTTPRequestHandler.__init__(self, *args) def do_GET(self): @@ -333,7 +417,7 @@ class GetHandler(BaseHTTPRequestHandler): caveats.extend(self._caveats) m = self._bakery.oven.macaroon( - version=bakery.LATEST_VERSION, expiry=AGES, + version=bakery.LATEST_VERSION, expiry=self._expiry, caveats=caveats, ops=[TEST_OP]) content, headers = httpbakery.discharge_required_response( diff --git a/macaroonbakery/tests/test_codec.py b/macaroonbakery/tests/test_codec.py index d4fbc57..d82a794 100644 --- a/macaroonbakery/tests/test_codec.py +++ b/macaroonbakery/tests/test_codec.py @@ -3,12 +3,11 @@ import base64 from unittest import TestCase +import macaroonbakery.bakery as bakery +import macaroonbakery.checkers as checkers import nacl.public import six - -import macaroonbakery as bakery -from macaroonbakery import codec -import macaroonbakery.checkers as checkers +from macaroonbakery.bakery import _codec as codec class TestCodec(TestCase): diff --git a/macaroonbakery/tests/test_discharge.py b/macaroonbakery/tests/test_discharge.py index 433483a..27bae63 100644 --- a/macaroonbakery/tests/test_discharge.py +++ b/macaroonbakery/tests/test_discharge.py @@ -2,11 +2,10 @@ # Licensed under the LGPLv3, see LICENCE file for details. import unittest -from pymacaroons import MACAROON_V1, Macaroon - -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers from macaroonbakery.tests import common +from pymacaroons import MACAROON_V1, Macaroon class TestDischarge(unittest.TestCase): diff --git a/macaroonbakery/tests/test_discharge_all.py b/macaroonbakery/tests/test_discharge_all.py index 7999f5f..cab8a07 100644 --- a/macaroonbakery/tests/test_discharge_all.py +++ b/macaroonbakery/tests/test_discharge_all.py @@ -2,11 +2,10 @@ # Licensed under the LGPLv3, see LICENCE file for details. import unittest -from pymacaroons.verifier import Verifier - -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers from macaroonbakery.tests import common +from pymacaroons.verifier import Verifier def always_ok(predicate): diff --git a/macaroonbakery/tests/test_keyring.py b/macaroonbakery/tests/test_keyring.py index 438ab1b..3503145 100644 --- a/macaroonbakery/tests/test_keyring.py +++ b/macaroonbakery/tests/test_keyring.py @@ -2,11 +2,11 @@ # Licensed under the LGPLv3, see LICENCE file for details. import unittest -from httmock import urlmatch, HTTMock - -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.httpbakery as httpbakery +from httmock import HTTMock, urlmatch + class TestKeyRing(unittest.TestCase): @@ -19,7 +19,7 @@ class TestKeyRing(unittest.TestCase): 'status_code': 200, 'content': { 'Version': bakery.LATEST_VERSION, - 'PublicKey': key.public_key.encode().decode('utf-8') + 'PublicKey': str(key.public_key), } } @@ -41,7 +41,7 @@ class TestKeyRing(unittest.TestCase): 'status_code': 200, 'content': { 'Version': bakery.LATEST_VERSION, - 'PublicKey': key.public_key.encode().decode('utf-8') + 'PublicKey': str(key.public_key), } } @@ -64,7 +64,7 @@ class TestKeyRing(unittest.TestCase): return { 'status_code': 200, 'content': { - 'PublicKey': key.public_key.encode().decode('utf-8') + 'PublicKey': str(key.public_key), } } @@ -79,7 +79,7 @@ class TestKeyRing(unittest.TestCase): def test_allow_insecure(self): kr = httpbakery.ThirdPartyLocator() - with self.assertRaises(bakery.error.ThirdPartyInfoNotFound): + with self.assertRaises(bakery.ThirdPartyInfoNotFound): kr.third_party_info('http://0.1.2.3/') def test_fallback(self): @@ -96,7 +96,7 @@ class TestKeyRing(unittest.TestCase): return { 'status_code': 200, 'content': { - 'PublicKey': key.public_key.encode().decode('utf-8') + 'PublicKey': str(key.public_key), } } diff --git a/macaroonbakery/tests/test_macaroon.py b/macaroonbakery/tests/test_macaroon.py index 93bbbb8..bcbbf80 100644 --- a/macaroonbakery/tests/test_macaroon.py +++ b/macaroonbakery/tests/test_macaroon.py @@ -3,13 +3,12 @@ import json from unittest import TestCase -import six -import pymacaroons -from pymacaroons import serializers - -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers +import pymacaroons +import six from macaroonbakery.tests import common +from pymacaroons import serializers class TestMacaroon(TestCase): @@ -25,7 +24,8 @@ class TestMacaroon(TestCase): self.assertEquals(m.version, bakery.LATEST_VERSION) def test_add_first_party_caveat(self): - m = bakery.Macaroon('rootkey', 'some id', 'here', bakery.LATEST_VERSION) + m = bakery.Macaroon('rootkey', 'some id', 'here', + bakery.LATEST_VERSION) m.add_caveat(checkers.Caveat('test_condition')) caveats = m.first_party_caveats() self.assertEquals(len(caveats), 1) diff --git a/macaroonbakery/tests/test_oven.py b/macaroonbakery/tests/test_oven.py index ae235de..3c29767 100644 --- a/macaroonbakery/tests/test_oven.py +++ b/macaroonbakery/tests/test_oven.py @@ -1,11 +1,10 @@ # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. -from unittest import TestCase - import copy from datetime import datetime, timedelta +from unittest import TestCase -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery EPOCH = datetime(1900, 11, 17, 19, 00, 13, 0, None) AGES = EPOCH + timedelta(days=10) @@ -88,7 +87,8 @@ class TestOven(TestCase): ops_store=bakery.MemoryOpsStore()) ops = [] for i in range(30000): - ops.append(bakery.Op(entity='entity' + str(i), action='action' + str(i))) + ops.append(bakery.Op(entity='entity' + str(i), + action='action' + str(i))) m = test_oven.macaroon(bakery.LATEST_VERSION, AGES, None, ops) diff --git a/macaroonbakery/tests/test_store.py b/macaroonbakery/tests/test_store.py index 5afa7be..8a54f59 100644 --- a/macaroonbakery/tests/test_store.py +++ b/macaroonbakery/tests/test_store.py @@ -2,7 +2,7 @@ # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase -import macaroonbakery as bakery +import macaroonbakery.bakery as bakery class TestOven(TestCase): diff --git a/macaroonbakery/tests/test_time.py b/macaroonbakery/tests/test_time.py index 38826e1..2685e56 100644 --- a/macaroonbakery/tests/test_time.py +++ b/macaroonbakery/tests/test_time.py @@ -1,16 +1,15 @@ # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. +from collections import namedtuple from datetime import timedelta from unittest import TestCase -from collections import namedtuple -import pyrfc3339 +import macaroonbakery.checkers as checkers import pymacaroons +import pyrfc3339 from pymacaroons import Macaroon -import macaroonbakery.checkers as checkers - -t1 = pyrfc3339.parse('2017-10-26T16:19:47.441402074Z') +t1 = pyrfc3339.parse('2017-10-26T16:19:47.441402074Z', produce_naive=True) t2 = t1 + timedelta(hours=1) t3 = t2 + timedelta(hours=1) @@ -118,9 +117,17 @@ class TestExpireTime(TestCase): ] for test in tests: print('test ', test.about) - t = checkers.macaroons_expiry_time(checkers.Namespace(), test.macaroons) + t = checkers.macaroons_expiry_time(checkers.Namespace(), + test.macaroons) self.assertEqual(t, test.expectTime) + def test_macaroons_expire_time_skips_third_party(self): + m1 = newMacaroon([checkers.time_before_caveat(t1).condition]) + m2 = newMacaroon() + m2.add_third_party_caveat('https://example.com', 'a-key', '123') + t = checkers.macaroons_expiry_time(checkers.Namespace(), [m1, m2]) + self.assertEqual(t1, t) + def newMacaroon(conds=[]): m = Macaroon(key='key', version=2) diff --git a/macaroonbakery/tests/test_utils.py b/macaroonbakery/tests/test_utils.py new file mode 100644 index 0000000..fcc8839 --- /dev/null +++ b/macaroonbakery/tests/test_utils.py @@ -0,0 +1,74 @@ +# Copyright 2017 Canonical Ltd. +# Licensed under the LGPLv3, see LICENCE file for details. + +import json +from datetime import datetime +from unittest import TestCase + +import macaroonbakery.bakery as bakery +import pymacaroons +import pytz +from macaroonbakery._utils import cookie +from pymacaroons.serializers import json_serializer + + +class CookieTest(TestCase): + + def test_cookie_expires_naive(self): + timestamp = datetime.utcnow() + c = cookie('http://example.com', 'test', 'value', expires=timestamp) + self.assertEqual( + c.expires, int((timestamp - datetime(1970, 1, 1)).total_seconds())) + + def test_cookie_expires_with_timezone(self): + timestamp = datetime.now(pytz.UTC) + self.assertRaises( + ValueError, cookie, 'http://example.com', 'test', 'value', + expires=timestamp) + + +class TestB64Decode(TestCase): + def test_decode(self): + test_cases = [{ + 'about': 'empty string', + 'input': '', + 'expect': '', + }, { + 'about': 'standard encoding, padded', + 'input': 'Z29+IQ==', + 'expect': 'go~!', + }, { + 'about': 'URL encoding, padded', + 'input': 'Z29-IQ==', + 'expect': 'go~!', + }, { + 'about': 'standard encoding, not padded', + 'input': 'Z29+IQ', + 'expect': 'go~!', + }, { + 'about': 'URL encoding, not padded', + 'input': 'Z29-IQ', + 'expect': 'go~!', + }, { + 'about': 'standard encoding, not enough much padding', + 'input': 'Z29+IQ=', + 'expect_error': 'illegal base64 data at input byte 8', + }] + for test in test_cases: + if test.get('expect_error'): + with self.assertRaises(ValueError, msg=test['about']) as e: + bakery.b64decode(test['input']) + self.assertEqual(str(e.exception), 'Incorrect padding') + else: + self.assertEqual(bakery.b64decode(test['input']), test['expect'].encode('utf-8'), msg=test['about']) + + +class MacaroonToDictTest(TestCase): + def test_macaroon_to_dict(self): + m = pymacaroons.Macaroon( + key=b'rootkey', identifier=b'some id', location='here', version=2) + as_dict = bakery.macaroon_to_dict(m) + data = json.dumps(as_dict) + m1 = pymacaroons.Macaroon.deserialize(data, json_serializer.JsonSerializer()) + self.assertEqual(m1.signature, m.signature) + pymacaroons.Verifier().verify(m1, b'rootkey') |