# Copyright 2017 Canonical Ltd.
# 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 httmock import HTTMock, response, urlmatch
ID_PATH = 'http://example.com/someprotecteurl'
json_macaroon = {
u'identifier': u'macaroon-identifier',
u'caveats': [
{
u'cl': u'http://example.com/identity/v1/discharger',
u'vid': u'zgtQa88oS9UF45DlJniRaAUT4qqHhLxQzCeUU9N2O1Uu-'
u'yhFulgGbSA0zDGdkrq8YNQAxGiARA_-AGxyoh25kiTycb8u47pD',
u'cid': u'eyJUaGlyZFBhcnR5UHV'
}, {
u'cid': u'allow read-no-terms write'
}, {
u'cid': u'time-before 2158-07-19T14:29:14.312669464Z'
}],
u'location': u'charmstore',
u'signature': u'52d17cb11f5c84d58441bc0ffd7cc396'
u'5115374ce2fa473ecf06265b5d4d9e81'
}
discharge_token = [{
u'identifier': u'token-identifier===',
u'caveats': [{
u'cid': u'declared username someone'
}, {
u'cid': u'time-before 2158-08-15T15:55:52.428319076Z'
}, {
u'cid': u'origin '
}],
u'location': u'https://example.com/identity',
u'signature': u'5ae0e7a2abf806bdd92f510fcd3'
u'198f520691259abe76ffae5623dae048769ef'
}]
discharged_macaroon = {
u'identifier': u'discharged-identifier=',
u'caveats': [{
u'cid': u'declared uuid a1130b10-3deb-59b7-baf0-c2a3f83e7382'
}, {
u'cid': u'declared username someone'
}, {
u'cid': u'time-before 2158-07-19T15:55:52.432439055Z'
}],
u'location': u'',
u'signature': u'3513db5503ab17f9576760cd28'
u'ce658ce8bf6b43038255969fc3c1cd8b172345'
}
@urlmatch(path='.*/someprotecteurl')
def first_407_then_200(url, request):
if request.headers.get('cookie', '').startswith('macaroon-'):
return {
'status_code': 200,
'content': {
'Value': 'some value'
}
}
else:
resp = response(status_code=407,
content={
'Info': {
'Macaroon': json_macaroon,
'MacaroonPath': '/',
'CookieNameSuffix': 'test'
},
'Message': 'verification failed: no macaroon '
'cookies in request',
'Code': 'macaroon discharge required'
},
headers={'Content-Type': 'application/json'})
return request.hooks['response'][0](resp)
@urlmatch(netloc='example.com:8000', path='.*/someprotecteurl')
def first_407_then_200_with_port(url, request):
if request.headers.get('cookie', '').startswith('macaroon-'):
return {
'status_code': 200,
'content': {
'Value': 'some value'
}
}
else:
resp = response(status_code=407,
content={
'Info': {
'Macaroon': json_macaroon,
'MacaroonPath': '/',
'CookieNameSuffix': 'test'
},
'Message': 'verification failed: no macaroon '
'cookies in request',
'Code': 'macaroon discharge required'
},
headers={'Content-Type': 'application/json'},
request=request)
return request.hooks['response'][0](resp)
@urlmatch(path='.*/someprotecteurl')
def valid_200(url, request):
return {
'status_code': 200,
'content': {
'Value': 'some value'
}
}
@urlmatch(path='.*/discharge')
def discharge_200(url, request):
return {
'status_code': 200,
'content': {
'Macaroon': discharged_macaroon
}
}
@urlmatch(path='.*/discharge')
def discharge_401(url, request):
return {
'status_code': 401,
'content': {
'Code': 'interaction required',
'Info': {
'VisitURL': 'http://example.com/visit',
'WaitURL': 'http://example.com/wait'
}
},
'headers': {
'WWW-Authenticate': 'Macaroon'
}
}
@urlmatch(path='.*/visit')
def visit_200(url, request):
return {
'status_code': 200,
'content': {
'interactive': '/visit'
}
}
@urlmatch(path='.*/wait')
def wait_after_401(url, request):
if request.url != 'http://example.com/wait':
return {'status_code': 500}
return {
'status_code': 200,
'content': {
'DischargeToken': discharge_token,
'Macaroon': discharged_macaroon
}
}
@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):
for cookie in cookies:
if cookie.name == name:
assert cookie.secure == secure
break
else:
assert False, 'no cookie named {} found in jar'.format(name)
def test_discharge(self):
client = httpbakery.Client()
with HTTMock(first_407_then_200), HTTMock(discharge_200):
resp = requests.get(ID_PATH,
cookies=client.cookies,
auth=client.auth())
resp.raise_for_status()
assert 'macaroon-test' in client.cookies.keys()
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):
resp = requests.get(
ID_PATH,
cookies=client.cookies,
auth=client.auth(),
)
resp.raise_for_status()
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),\
HTTMock(visit_200):
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):
with HTTMock(discharge_200):
resp = requests.get('http://example.com:8000/someprotecteurl',
cookies=client.cookies,
auth=client.auth())
resp.raise_for_status()
assert 'macaroon-test' in client.cookies.keys()
def test_secure_cookie_for_https(self):
client = httpbakery.Client()
with HTTMock(first_407_then_200_with_port), HTTMock(discharge_200):
resp = requests.get(
'https://example.com:8000/someprotecteurl',
cookies=client.cookies,
auth=client.auth())
resp.raise_for_status()
assert 'macaroon-test' in client.cookies.keys()
self.assert_cookie_security(client.cookies, 'macaroon-test',
secure=True)