Merge lp:~facundo/canonical-identity-provider/new-era-discharge into lp:canonical-identity-provider/release
- new-era-discharge
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1440 |
Proposed branch: | lp:~facundo/canonical-identity-provider/new-era-discharge |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
952 lines (+461/-107) 9 files modified
src/api/v20/handlers.py (+41/-16) src/api/v20/tests/test_handlers.py (+103/-18) src/identityprovider/auth.py (+84/-21) src/identityprovider/tests/test_auth.py (+219/-38) src/identityprovider/tests/test_forms.py (+6/-6) src/identityprovider/tests/test_macaroon.py (+1/-1) src/identityprovider/tests/test_views_server.py (+4/-4) src/identityprovider/tests/utils.py (+1/-1) src/identityprovider/views/server.py (+2/-2) |
To merge this branch: | bzr merge lp:~facundo/canonical-identity-provider/new-era-discharge |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | Needs Fixing | ||
Facundo Batista (community) | Approve | ||
Ricardo Kirkner (community) | Approve | ||
Review via email:
|
Commit message
Accept the caveat_id (instead of macaroon) and return the unbound discharge (instead of binding it) for both create and refresh discharges.
Description of the change
Accept the caveat_id (instead of macaroon) and return the unbound discharge (instead of binding it) for both create and refresh discharges.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Facundo Batista (facundo) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
The attempt to merge lp:~facundo/canonical-identity-provider/new-era-discharge into lp:canonical-identity-provider failed. Below is the output from the failed tests.
Bootstrapping...
rm -rf /mnt/tarmac/
rm -rf branches/wheels
rm -rf branches/*
rm -rf staticfiles
rm -f lib/versioninfo.py
find -name '*.pyc' -delete
find -name '*.~*' -delete
Not deleting /mnt/tarmac/
New python executable in /mnt/tarmac/
Installing setuptools, pip...done.
/usr/lib/
touch branches/last_build
[ -d branches/wheels ] && (cd branches/wheels && bzr pull) || (bzr branch lp:~ubuntuone-pqm-team/canonical-identity-provider/dependencies branches/wheels)
bzr version-info --format=python > lib/versioninfo.py
/mnt/tarmac/
Ignoring indexes: https:/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Requirement already satisfied (use --upgrade to upgrade): oops==0.0.13 in /usr/lib/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Downloading/
Requirement already s...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
William Grant (wgrant) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Facundo Batista (facundo) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
William Grant (wgrant) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Facundo Batista (facundo) wrote : | # |
Addressed the "no caveat id in refreshing" issue
Preview Diff
1 | === modified file 'src/api/v20/handlers.py' | |||
2 | --- src/api/v20/handlers.py 2016-04-18 15:53:18 +0000 | |||
3 | +++ src/api/v20/handlers.py 2016-05-05 12:45:29 +0000 | |||
4 | @@ -58,6 +58,7 @@ | |||
5 | 58 | TokenScope, | 58 | TokenScope, |
6 | 59 | ) | 59 | ) |
7 | 60 | from identityprovider.signals import login_failed, login_succeeded | 60 | from identityprovider.signals import login_failed, login_succeeded |
8 | 61 | from identityprovider.stats import stats | ||
9 | 61 | from identityprovider.timeline_helpers import get_request_timing_function | 62 | from identityprovider.timeline_helpers import get_request_timing_function |
10 | 62 | from identityprovider.utils import redirection_url_for_token | 63 | from identityprovider.utils import redirection_url_for_token |
11 | 63 | 64 | ||
12 | @@ -446,11 +447,19 @@ | |||
13 | 446 | if 'macaroon' in data: | 447 | if 'macaroon' in data: |
14 | 447 | simple_macaroon = True | 448 | simple_macaroon = True |
15 | 448 | root_macaroons_info = [(None, data['macaroon'])] | 449 | root_macaroons_info = [(None, data['macaroon'])] |
16 | 450 | stats.increment('macaroon.discharge', key='single_macaroon') | ||
17 | 449 | elif 'macaroons' in data: | 451 | elif 'macaroons' in data: |
18 | 450 | simple_macaroon = False | 452 | simple_macaroon = False |
19 | 451 | root_macaroons_info = data['macaroons'] | 453 | root_macaroons_info = data['macaroons'] |
20 | 454 | stats.increment('macaroon.discharge', key='multi_macaroons') | ||
21 | 455 | elif 'caveat_id' in data: | ||
22 | 456 | simple_macaroon = True | ||
23 | 457 | root_macaroons_info = None | ||
24 | 458 | caveat_id = data['caveat_id'] | ||
25 | 459 | stats.increment('macaroon.discharge', key='caveat_id') | ||
26 | 452 | else: | 460 | else: |
28 | 453 | missing = {'macaroon or macaroons': [FIELD_REQUIRED]} | 461 | # let's flag the new one only, not the deprecated ones |
29 | 462 | missing = {'caveat_id': [FIELD_REQUIRED]} | ||
30 | 454 | return errors.INVALID_DATA(**missing) | 463 | return errors.INVALID_DATA(**missing) |
31 | 455 | 464 | ||
32 | 456 | account = None | 465 | account = None |
33 | @@ -490,16 +499,22 @@ | |||
34 | 490 | if response is not None: | 499 | if response is not None: |
35 | 491 | return response | 500 | return response |
36 | 492 | 501 | ||
47 | 493 | all_discharges = [] | 502 | try: |
48 | 494 | for mac_id, root_macaroon_raw in root_macaroons_info: | 503 | if root_macaroons_info is None: |
49 | 495 | try: | 504 | discharge = auth.build_discharge_macaroon(account, caveat_id) |
50 | 496 | discharge = auth.build_discharge_macaroon( | 505 | all_discharges = [(None, discharge.serialize())] |
51 | 497 | account, root_macaroon_raw) | 506 | else: |
52 | 498 | except ValidationError: | 507 | # 2016-05-03: this is deprecated, just supporting it until |
53 | 499 | return errors.INVALID_DATA() | 508 | # no more clients send root macaroon(s) |
54 | 500 | except AuthenticationError: | 509 | all_discharges = [] |
55 | 501 | return errors.INVALID_CREDENTIALS() | 510 | for mac_id, root_macaroon_raw in root_macaroons_info: |
56 | 502 | all_discharges.append((mac_id, discharge.serialize())) | 511 | discharge = auth.build_discharge_macaroon_from_root( |
57 | 512 | account, root_macaroon_raw) | ||
58 | 513 | all_discharges.append((mac_id, discharge.serialize())) | ||
59 | 514 | except ValidationError: | ||
60 | 515 | return errors.INVALID_DATA() | ||
61 | 516 | except AuthenticationError: | ||
62 | 517 | return errors.INVALID_CREDENTIALS() | ||
63 | 503 | 518 | ||
64 | 504 | response = rc.ALL_OK | 519 | response = rc.ALL_OK |
65 | 505 | if simple_macaroon: | 520 | if simple_macaroon: |
66 | @@ -523,16 +538,26 @@ | |||
67 | 523 | 538 | ||
68 | 524 | data = request.data | 539 | data = request.data |
69 | 525 | try: | 540 | try: |
70 | 526 | root_macaroon_raw = data['root_macaroon'] | ||
71 | 527 | discharge_macaroon_raw = data['discharge_macaroon'] | 541 | discharge_macaroon_raw = data['discharge_macaroon'] |
72 | 528 | except KeyError: | 542 | except KeyError: |
75 | 529 | expected = {'root_macaroon', 'discharge_macaroon'} | 543 | missing = {'discharge_macaroon': [FIELD_REQUIRED]} |
76 | 530 | missing = dict((k, [FIELD_REQUIRED]) for k in expected - set(data)) | 544 | return errors.INVALID_DATA(**missing) |
77 | 545 | |||
78 | 546 | if 'caveat_id' in data: | ||
79 | 547 | method = auth.refresh_discharge | ||
80 | 548 | params = (data['caveat_id'], discharge_macaroon_raw) | ||
81 | 549 | stats.increment('macaroon.refresh', key='caveat_id') | ||
82 | 550 | elif 'root_macaroon' in data: | ||
83 | 551 | method = auth.refresh_macaroons | ||
84 | 552 | params = (data['root_macaroon'], discharge_macaroon_raw) | ||
85 | 553 | stats.increment('macaroon.refresh', key='macaroon') | ||
86 | 554 | else: | ||
87 | 555 | # let's flag the new one only, not the deprecated one | ||
88 | 556 | missing = {'caveat_id': [FIELD_REQUIRED]} | ||
89 | 531 | return errors.INVALID_DATA(**missing) | 557 | return errors.INVALID_DATA(**missing) |
90 | 532 | 558 | ||
91 | 533 | try: | 559 | try: |
94 | 534 | new_discharge = auth.refresh_macaroons(root_macaroon_raw, | 560 | new_discharge = method(*params) |
93 | 535 | discharge_macaroon_raw) | ||
95 | 536 | except ValidationError: | 561 | except ValidationError: |
96 | 537 | return errors.INVALID_DATA() | 562 | return errors.INVALID_DATA() |
97 | 538 | except AccountDeactivated: | 563 | except AccountDeactivated: |
98 | 539 | 564 | ||
99 | === modified file 'src/api/v20/tests/test_handlers.py' | |||
100 | --- src/api/v20/tests/test_handlers.py 2016-04-18 15:53:18 +0000 | |||
101 | +++ src/api/v20/tests/test_handlers.py 2016-05-05 12:45:29 +0000 | |||
102 | @@ -2169,13 +2169,25 @@ | |||
103 | 2169 | self.login_failed_calls = [] | 2169 | self.login_failed_calls = [] |
104 | 2170 | login_failed.connect(self.track_failed_logins, dispatch_uid=self.id()) | 2170 | login_failed.connect(self.track_failed_logins, dispatch_uid=self.id()) |
105 | 2171 | 2171 | ||
107 | 2172 | self.root_macaroon, self.macaroon_random_key = self.build_macaroon() | 2172 | self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon() |
108 | 2173 | self.data = dict( | 2173 | self.data = dict( |
109 | 2174 | email='foo@bar.com', password='foobar123', | 2174 | email='foo@bar.com', password='foobar123', |
110 | 2175 | macaroon=self.root_macaroon.serialize()) | 2175 | macaroon=self.root_macaroon.serialize()) |
111 | 2176 | self.account = self.factory.make_account( | 2176 | self.account = self.factory.make_account( |
112 | 2177 | email=self.data['email'], password=self.data['password']) | 2177 | email=self.data['email'], password=self.data['password']) |
113 | 2178 | 2178 | ||
114 | 2179 | self._stats_increment = self.patch('api.v20.handlers.stats.increment') | ||
115 | 2180 | |||
116 | 2181 | def check_stats_increment(self, *args, **kwargs): | ||
117 | 2182 | """Check stats increment was properly called.""" | ||
118 | 2183 | calls = self._stats_increment.call_args_list | ||
119 | 2184 | self.assertEqual(len(calls), 1) | ||
120 | 2185 | call_args, call_kwargs = calls[0] | ||
121 | 2186 | self.assertEqual(call_args, args, | ||
122 | 2187 | "Bad args in stats increment call {}".format(calls)) | ||
123 | 2188 | self.assertEqual(call_kwargs, kwargs, | ||
124 | 2189 | "Bad kwargs in stats increment call {}".format(calls)) | ||
125 | 2190 | |||
126 | 2179 | 2191 | ||
127 | 2180 | class MacaroonDischargeHandlerTestCase(MacaroonHandlerBaseTestCase, | 2192 | class MacaroonDischargeHandlerTestCase(MacaroonHandlerBaseTestCase, |
128 | 2181 | AuthLogTestCaseMixin): | 2193 | AuthLogTestCaseMixin): |
129 | @@ -2199,7 +2211,7 @@ | |||
130 | 2199 | response) | 2211 | response) |
131 | 2200 | return json.loads(response.content) | 2212 | return json.loads(response.content) |
132 | 2201 | 2213 | ||
134 | 2202 | def assert_response_correct(self, json_body, account): | 2214 | def assert_response_correct(self, json_body, account, bind=False): |
135 | 2203 | """Check we received a good discharge macaroon for the root sent. | 2215 | """Check we received a good discharge macaroon for the root sent. |
136 | 2204 | 2216 | ||
137 | 2205 | Proper verification of the received macaroon internals happens in | 2217 | Proper verification of the received macaroon internals happens in |
138 | @@ -2207,6 +2219,9 @@ | |||
139 | 2207 | """ | 2219 | """ |
140 | 2208 | discharge_macaroon_raw = json_body['discharge_macaroon'] | 2220 | discharge_macaroon_raw = json_body['discharge_macaroon'] |
141 | 2209 | discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw) | 2221 | discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw) |
142 | 2222 | if bind: | ||
143 | 2223 | discharge_macaroon = self.root_macaroon.prepare_for_request( | ||
144 | 2224 | discharge_macaroon) | ||
145 | 2210 | v = Verifier() | 2225 | v = Verifier() |
146 | 2211 | v.satisfy_general(lambda c: True) | 2226 | v.satisfy_general(lambda c: True) |
147 | 2212 | v.verify( | 2227 | v.verify( |
148 | @@ -2327,6 +2342,7 @@ | |||
149 | 2327 | def test_macaroon_created(self): | 2342 | def test_macaroon_created(self): |
150 | 2328 | json_body = self.do_post() | 2343 | json_body = self.do_post() |
151 | 2329 | self.assert_response_correct(json_body, self.account) | 2344 | self.assert_response_correct(json_body, self.account) |
152 | 2345 | self.check_stats_increment('macaroon.discharge', key='single_macaroon') | ||
153 | 2330 | 2346 | ||
154 | 2331 | def test_root_macaroon_corrupt(self): | 2347 | def test_root_macaroon_corrupt(self): |
155 | 2332 | data = dict( | 2348 | data = dict( |
156 | @@ -2337,23 +2353,23 @@ | |||
157 | 2337 | check_login_failed=False) | 2353 | check_login_failed=False) |
158 | 2338 | 2354 | ||
159 | 2339 | def test_root_macaroon_not_for_sso(self): | 2355 | def test_root_macaroon_not_for_sso(self): |
161 | 2340 | macaroon, _ = self.build_macaroon(service_location="other service") | 2356 | macaroon, _, _ = self.build_macaroon(service_location="other service") |
162 | 2341 | data = dict(email='foo@bar.com', password='foobar123', | 2357 | data = dict(email='foo@bar.com', password='foobar123', |
163 | 2342 | macaroon=macaroon.serialize()) | 2358 | macaroon=macaroon.serialize()) |
164 | 2343 | self.assert_failed_login('INVALID_CREDENTIALS', data, | 2359 | self.assert_failed_login('INVALID_CREDENTIALS', data, |
165 | 2344 | expected_status_code=401, | 2360 | expected_status_code=401, |
166 | 2345 | check_login_failed=False) | 2361 | check_login_failed=False) |
167 | 2346 | 2362 | ||
169 | 2347 | def test_missing_macaroons(self): | 2363 | def test_missing_macaroon_info(self): |
170 | 2348 | data = dict(email='foo@bar.com', password='foobar123') | 2364 | data = dict(email='foo@bar.com', password='foobar123') |
172 | 2349 | extra = {'macaroon or macaroons': [handlers.FIELD_REQUIRED]} | 2365 | extra = {'caveat_id': [handlers.FIELD_REQUIRED]} |
173 | 2350 | self.assert_failed_login('INVALID_DATA', data, extra=extra, | 2366 | self.assert_failed_login('INVALID_DATA', data, extra=extra, |
174 | 2351 | expected_status_code=400, | 2367 | expected_status_code=400, |
175 | 2352 | check_login_failed=False) | 2368 | check_login_failed=False) |
176 | 2353 | 2369 | ||
177 | 2354 | def test_multiple_macaroons(self): | 2370 | def test_multiple_macaroons(self): |
178 | 2355 | # build *several* macaroons and send them all at once | 2371 | # build *several* macaroons and send them all at once |
180 | 2356 | roots, rkeys = zip(*[self.build_macaroon() for _ in range(3)]) | 2372 | roots, rkeys, _ = zip(*[self.build_macaroon() for _ in range(3)]) |
181 | 2357 | ids = range(3) | 2373 | ids = range(3) |
182 | 2358 | roots = dict(zip(ids, roots)) | 2374 | roots = dict(zip(ids, roots)) |
183 | 2359 | rkeys = dict(zip(ids, rkeys)) | 2375 | rkeys = dict(zip(ids, rkeys)) |
184 | @@ -2372,8 +2388,11 @@ | |||
185 | 2372 | v.satisfy_general(verifying_function) | 2388 | v.satisfy_general(verifying_function) |
186 | 2373 | v.verify(root_macaroon, key, [discharge_macaroon]) | 2389 | v.verify(root_macaroon, key, [discharge_macaroon]) |
187 | 2374 | 2390 | ||
188 | 2391 | self.check_stats_increment('macaroon.discharge', key='multi_macaroons') | ||
189 | 2392 | |||
190 | 2375 | def test_multiple_mixed(self): | 2393 | def test_multiple_mixed(self): |
192 | 2376 | bad_macaroon, _ = self.build_macaroon(service_location="other service") | 2394 | bad_macaroon, _, _ = self.build_macaroon( |
193 | 2395 | service_location="other service") | ||
194 | 2377 | data = dict(email='foo@bar.com', password='foobar123', macaroons=[ | 2396 | data = dict(email='foo@bar.com', password='foobar123', macaroons=[ |
195 | 2378 | ('good', self.root_macaroon.serialize()), | 2397 | ('good', self.root_macaroon.serialize()), |
196 | 2379 | ('bad', bad_macaroon.serialize()), | 2398 | ('bad', bad_macaroon.serialize()), |
197 | @@ -2382,6 +2401,24 @@ | |||
198 | 2382 | expected_status_code=401, | 2401 | expected_status_code=401, |
199 | 2383 | check_login_failed=False) | 2402 | check_login_failed=False) |
200 | 2384 | 2403 | ||
201 | 2404 | def test_caveat_id_discharge_created(self): | ||
202 | 2405 | # discharge the test macaroon | ||
203 | 2406 | (caveat,) = [c for c in self.root_macaroon.third_party_caveats() | ||
204 | 2407 | if c.location == settings.MACAROON_SERVICE_LOCATION] | ||
205 | 2408 | data = dict(email='foo@bar.com', password='foobar123', | ||
206 | 2409 | caveat_id=caveat.caveat_id) | ||
207 | 2410 | |||
208 | 2411 | json_body = self.do_post(data=data) | ||
209 | 2412 | self.assert_response_correct(json_body, self.account, bind=True) | ||
210 | 2413 | self.check_stats_increment('macaroon.discharge', key='caveat_id') | ||
211 | 2414 | |||
212 | 2415 | def test_caveat_id_corrupt(self): | ||
213 | 2416 | data = dict(email='foo@bar.com', password='foobar123', | ||
214 | 2417 | caveat_id="I'm a seriously corrupted caveat") | ||
215 | 2418 | self.assert_failed_login('INVALID_DATA', data, | ||
216 | 2419 | expected_status_code=400, | ||
217 | 2420 | check_login_failed=False) | ||
218 | 2421 | |||
219 | 2385 | 2422 | ||
220 | 2386 | class MacaroonHandlerTimelineTestCase(MacaroonHandlerBaseTestCase, | 2423 | class MacaroonHandlerTimelineTestCase(MacaroonHandlerBaseTestCase, |
221 | 2387 | TimelineActionMixin): | 2424 | TimelineActionMixin): |
222 | @@ -2428,12 +2465,16 @@ | |||
223 | 2428 | super(MacaroonRefreshHandlerTestCase, self).setUp() | 2465 | super(MacaroonRefreshHandlerTestCase, self).setUp() |
224 | 2429 | 2466 | ||
225 | 2430 | # discharge the test macaroon | 2467 | # discharge the test macaroon |
232 | 2431 | root_macaroon_raw = self.root_macaroon.serialize() | 2468 | (caveat,) = [c for c in self.root_macaroon.third_party_caveats() |
233 | 2432 | self.discharge_macaroon = build_discharge_macaroon( | 2469 | if c.location == settings.MACAROON_SERVICE_LOCATION] |
234 | 2433 | self.account, root_macaroon_raw) | 2470 | |
235 | 2434 | 2471 | unbound_discharge = build_discharge_macaroon( | |
236 | 2435 | self.data = {'root_macaroon': root_macaroon_raw, | 2472 | self.account, caveat.caveat_id) |
237 | 2436 | 'discharge_macaroon': self.discharge_macaroon.serialize()} | 2473 | self.discharge_macaroon = self.root_macaroon.prepare_for_request( |
238 | 2474 | unbound_discharge) | ||
239 | 2475 | |||
240 | 2476 | self.data = {'caveat_id': caveat.caveat_id, | ||
241 | 2477 | 'discharge_macaroon': unbound_discharge.serialize()} | ||
242 | 2437 | 2478 | ||
243 | 2438 | def do_post(self, data=None, expected_status_code=200, | 2479 | def do_post(self, data=None, expected_status_code=200, |
244 | 2439 | content_type='application/json', **kwargs): | 2480 | content_type='application/json', **kwargs): |
245 | @@ -2463,13 +2504,23 @@ | |||
246 | 2463 | self.assertEqual(json_body['extra'], extra) | 2504 | self.assertEqual(json_body['extra'], extra) |
247 | 2464 | return json_body | 2505 | return json_body |
248 | 2465 | 2506 | ||
250 | 2466 | def test_required_parameters(self): | 2507 | def test_required_parameters_discharge(self): |
251 | 2467 | json_body = self.do_post(expected_status_code=400, data={}) | 2508 | json_body = self.do_post(expected_status_code=400, data={}) |
252 | 2468 | 2509 | ||
253 | 2469 | self.assertEqual(json_body, { | 2510 | self.assertEqual(json_body, { |
254 | 2470 | 'code': 'INVALID_DATA', | 2511 | 'code': 'INVALID_DATA', |
257 | 2471 | 'extra': {'root_macaroon': [handlers.FIELD_REQUIRED], | 2512 | 'extra': {'discharge_macaroon': [handlers.FIELD_REQUIRED]}, |
258 | 2472 | 'discharge_macaroon': [handlers.FIELD_REQUIRED]}, | 2513 | 'message': 'Invalid request data'}, |
259 | 2514 | ) | ||
260 | 2515 | self.assertEqual(self.login_failed_calls, []) | ||
261 | 2516 | |||
262 | 2517 | def test_required_parameters_caveat_id(self): | ||
263 | 2518 | json_body = self.do_post(expected_status_code=400, | ||
264 | 2519 | data={'discharge_macaroon': 'whatever'}) | ||
265 | 2520 | |||
266 | 2521 | self.assertEqual(json_body, { | ||
267 | 2522 | 'code': 'INVALID_DATA', | ||
268 | 2523 | 'extra': {'caveat_id': [handlers.FIELD_REQUIRED]}, | ||
269 | 2473 | 'message': 'Invalid request data'}, | 2524 | 'message': 'Invalid request data'}, |
270 | 2474 | ) | 2525 | ) |
271 | 2475 | self.assertEqual(self.login_failed_calls, []) | 2526 | self.assertEqual(self.login_failed_calls, []) |
272 | @@ -2486,7 +2537,7 @@ | |||
273 | 2486 | check_login_failed=False) | 2537 | check_login_failed=False) |
274 | 2487 | 2538 | ||
275 | 2488 | def test_macaroon_bad_authinfo(self): | 2539 | def test_macaroon_bad_authinfo(self): |
277 | 2489 | macaroon, _ = self.build_macaroon(service_location="other service") | 2540 | macaroon, _, _ = self.build_macaroon(service_location="other service") |
278 | 2490 | data = dict(root_macaroon=macaroon.serialize(), | 2541 | data = dict(root_macaroon=macaroon.serialize(), |
279 | 2491 | discharge_macaroon=self.discharge_macaroon.serialize()) | 2542 | discharge_macaroon=self.discharge_macaroon.serialize()) |
280 | 2492 | self.assert_failed_login('INVALID_CREDENTIALS', data, | 2543 | self.assert_failed_login('INVALID_CREDENTIALS', data, |
281 | @@ -2494,7 +2545,9 @@ | |||
282 | 2494 | check_login_failed=False) | 2545 | check_login_failed=False) |
283 | 2495 | 2546 | ||
284 | 2496 | def test_macaroon_refreshed(self): | 2547 | def test_macaroon_refreshed(self): |
286 | 2497 | json_body = self.do_post() | 2548 | data = {'root_macaroon': self.root_macaroon.serialize(), |
287 | 2549 | 'discharge_macaroon': self.discharge_macaroon.serialize()} | ||
288 | 2550 | json_body = self.do_post(data=data) | ||
289 | 2498 | 2551 | ||
290 | 2499 | # get new discharge macaroon and verify with old root (its internals | 2552 | # get new discharge macaroon and verify with old root (its internals |
291 | 2500 | # are verified by the tests of the 'refresh_macaroons' function itself) | 2553 | # are verified by the tests of the 'refresh_macaroons' function itself) |
292 | @@ -2504,3 +2557,35 @@ | |||
293 | 2504 | v.satisfy_general(lambda c: True) | 2557 | v.satisfy_general(lambda c: True) |
294 | 2505 | v.verify( | 2558 | v.verify( |
295 | 2506 | self.root_macaroon, self.macaroon_random_key, [discharge_macaroon]) | 2559 | self.root_macaroon, self.macaroon_random_key, [discharge_macaroon]) |
296 | 2560 | self.check_stats_increment('macaroon.refresh', key='macaroon') | ||
297 | 2561 | |||
298 | 2562 | def test_caveat_id_corrupt(self): | ||
299 | 2563 | data = dict(caveat_id="I'm a seriously corrupted caveat", | ||
300 | 2564 | discharge_macaroon="Also broken") | ||
301 | 2565 | self.assert_failed_login('INVALID_DATA', data, | ||
302 | 2566 | expected_status_code=400, | ||
303 | 2567 | check_login_failed=False) | ||
304 | 2568 | |||
305 | 2569 | def test_caveat_id_bad_authinfo(self): | ||
306 | 2570 | macaroon, _, _ = self.build_macaroon(service_location="other service") | ||
307 | 2571 | (caveat,) = [c for c in macaroon.third_party_caveats() | ||
308 | 2572 | if c.location == "other service"] | ||
309 | 2573 | data = dict(caveat_id=caveat.caveat_id, | ||
310 | 2574 | discharge_macaroon=self.discharge_macaroon.serialize()) | ||
311 | 2575 | self.assert_failed_login('INVALID_CREDENTIALS', data, | ||
312 | 2576 | expected_status_code=401, | ||
313 | 2577 | check_login_failed=False) | ||
314 | 2578 | |||
315 | 2579 | def test_caveat_id_refreshed(self): | ||
316 | 2580 | json_body = self.do_post() | ||
317 | 2581 | |||
318 | 2582 | # get new discharge macaroon, apply it and verify with old root (its | ||
319 | 2583 | # internals are verified by the tests of the 'refresh_macaroons' | ||
320 | 2584 | # function itself) | ||
321 | 2585 | unbound_discharge_raw = json_body['discharge_macaroon'] | ||
322 | 2586 | unbound_discharge = Macaroon.deserialize(unbound_discharge_raw) | ||
323 | 2587 | discharge = self.root_macaroon.prepare_for_request(unbound_discharge) | ||
324 | 2588 | v = Verifier() | ||
325 | 2589 | v.satisfy_general(lambda c: True) | ||
326 | 2590 | v.verify(self.root_macaroon, self.macaroon_random_key, [discharge]) | ||
327 | 2591 | self.check_stats_increment('macaroon.refresh', key='caveat_id') | ||
328 | 2507 | 2592 | ||
329 | === modified file 'src/identityprovider/auth.py' | |||
330 | --- src/identityprovider/auth.py 2016-03-23 16:20:24 +0000 | |||
331 | +++ src/identityprovider/auth.py 2016-05-05 12:45:29 +0000 | |||
332 | @@ -507,16 +507,21 @@ | |||
333 | 507 | # the macaroon doesn't have a location for this service | 507 | # the macaroon doesn't have a location for this service |
334 | 508 | raise AuthenticationError("The received macaroon is not for ours") | 508 | raise AuthenticationError("The received macaroon is not for ours") |
335 | 509 | 509 | ||
337 | 510 | # get the only-for-this-project random key | 510 | caveat_info = _decrypt_caveat(sso_caveat.caveat_id) |
338 | 511 | return sso_caveat.caveat_id, caveat_info | ||
339 | 512 | |||
340 | 513 | |||
341 | 514 | def _decrypt_caveat(raw_caveat_id): | ||
342 | 515 | """Decrypt and decode the caveat id info.""" | ||
343 | 511 | try: | 516 | try: |
344 | 512 | caveat_info_raw = settings.CRYPTO_SSO_PRIVKEY.decrypt( | 517 | caveat_info_raw = settings.CRYPTO_SSO_PRIVKEY.decrypt( |
346 | 513 | base64.b64decode(sso_caveat.caveat_id)) | 518 | base64.b64decode(raw_caveat_id)) |
347 | 514 | caveat_info = json.loads(caveat_info_raw) | 519 | caveat_info = json.loads(caveat_info_raw) |
348 | 515 | except: | 520 | except: |
349 | 516 | # not properly encrypted information inside | 521 | # not properly encrypted information inside |
351 | 517 | raise AuthenticationError("Bad info in the caveat_id") | 522 | raise ValidationError("Bad info in the caveat_id") |
352 | 518 | 523 | ||
354 | 519 | return sso_caveat.caveat_id, caveat_info | 524 | return caveat_info |
355 | 520 | 525 | ||
356 | 521 | 526 | ||
357 | 522 | def _build_discharge(macaroon_key, caveat_id, account, | 527 | def _build_discharge(macaroon_key, caveat_id, account, |
358 | @@ -556,8 +561,12 @@ | |||
359 | 556 | return d | 561 | return d |
360 | 557 | 562 | ||
361 | 558 | 563 | ||
364 | 559 | def build_discharge_macaroon(account, root_macaroon_raw): | 564 | def build_discharge_macaroon_from_root(account, root_macaroon_raw): |
365 | 560 | """Build a discharge macaroon from a root one.""" | 565 | """Build a discharge macaroon from a root one. |
366 | 566 | |||
367 | 567 | This function is deprecated; will be removed when clients stop hitting | ||
368 | 568 | the handler passing the root macaroon. | ||
369 | 569 | """ | ||
370 | 561 | try: | 570 | try: |
371 | 562 | root_macaroon = Macaroon.deserialize(root_macaroon_raw) | 571 | root_macaroon = Macaroon.deserialize(root_macaroon_raw) |
372 | 563 | except: | 572 | except: |
373 | @@ -576,18 +585,24 @@ | |||
374 | 576 | return discharge | 585 | return discharge |
375 | 577 | 586 | ||
376 | 578 | 587 | ||
379 | 579 | def refresh_macaroons(root_macaroon_raw, discharge_macaroon_raw): | 588 | def build_discharge_macaroon(account, raw_caveat_id): |
380 | 580 | """Refresh a root/discharge pair with a new discharge macaroon.""" | 589 | """Build a discharge macaroon from the caveat id.""" |
381 | 590 | # get the decrypted deserialized info | ||
382 | 591 | caveat_info = _decrypt_caveat(raw_caveat_id) | ||
383 | 592 | |||
384 | 593 | # create a discharge macaroon with same location, key and | ||
385 | 594 | # identifier than it's original 3rd-party caveat (so they can | ||
386 | 595 | # be matched and verified) | ||
387 | 596 | discharge = _build_discharge( | ||
388 | 597 | caveat_info['3rdparty'], raw_caveat_id, account) | ||
389 | 598 | |||
390 | 599 | # return the unbound discharge macaroon | ||
391 | 600 | return discharge | ||
392 | 601 | |||
393 | 602 | |||
394 | 603 | def _account_from_macaroon(macaroon, key, discharge=None): | ||
395 | 604 | """Get the new discharge macaroon.""" | ||
396 | 581 | service_location = settings.MACAROON_SERVICE_LOCATION | 605 | service_location = settings.MACAROON_SERVICE_LOCATION |
397 | 582 | |||
398 | 583 | try: | ||
399 | 584 | root_macaroon = Macaroon.deserialize(root_macaroon_raw) | ||
400 | 585 | discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw) | ||
401 | 586 | except: | ||
402 | 587 | raise ValidationError("The received Macaroons are corrupt") | ||
403 | 588 | |||
404 | 589 | # get the raw caveat id and the decrypted deserialized info | ||
405 | 590 | raw_caveat_id, caveat_info = _get_own_caveat(root_macaroon) | ||
406 | 591 | info_holder = {} | 606 | info_holder = {} |
407 | 592 | 607 | ||
408 | 593 | def checker(caveat): | 608 | def checker(caveat): |
409 | @@ -606,7 +621,10 @@ | |||
410 | 606 | v = Verifier() | 621 | v = Verifier() |
411 | 607 | v.satisfy_general(checker) | 622 | v.satisfy_general(checker) |
412 | 608 | try: | 623 | try: |
414 | 609 | v.verify(root_macaroon, caveat_info['roothash'], [discharge_macaroon]) | 624 | if discharge is None: |
415 | 625 | v.verify(macaroon, key) | ||
416 | 626 | else: | ||
417 | 627 | v.verify(macaroon, key, [discharge]) | ||
418 | 610 | except: | 628 | except: |
419 | 611 | raise AuthenticationError("Not verifying macaroons") | 629 | raise AuthenticationError("Not verifying macaroons") |
420 | 612 | 630 | ||
421 | @@ -620,14 +638,59 @@ | |||
422 | 620 | if account.accountpassword.date_changed > last_auth_ts: | 638 | if account.accountpassword.date_changed > last_auth_ts: |
423 | 621 | raise AuthenticationError("Password changed") | 639 | raise AuthenticationError("Password changed") |
424 | 622 | 640 | ||
425 | 641 | return account, info_holder | ||
426 | 642 | |||
427 | 643 | |||
428 | 644 | def refresh_macaroons(root_macaroon_raw, discharge_macaroon_raw): | ||
429 | 645 | """Refresh a root/discharge pair with a new discharge macaroon. | ||
430 | 646 | |||
431 | 647 | This function is deprecated; will be removed when clients stop hitting | ||
432 | 648 | the handler passing the root macaroon. | ||
433 | 649 | """ | ||
434 | 650 | try: | ||
435 | 651 | root_macaroon = Macaroon.deserialize(root_macaroon_raw) | ||
436 | 652 | discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw) | ||
437 | 653 | except: | ||
438 | 654 | raise ValidationError("The received Macaroons are corrupt") | ||
439 | 655 | |||
440 | 656 | # get the raw caveat id and the decrypted deserialized info | ||
441 | 657 | raw_caveat_id, caveat_info = _get_own_caveat(root_macaroon) | ||
442 | 658 | |||
443 | 659 | account, macaroon_info = _account_from_macaroon( | ||
444 | 660 | root_macaroon, caveat_info['roothash'], discharge_macaroon) | ||
445 | 661 | |||
446 | 623 | # create the new discharge macaroon with same location, key and | 662 | # create the new discharge macaroon with same location, key and |
447 | 624 | # identifier than it's original 3rd-party caveat (so they can | 663 | # identifier than it's original 3rd-party caveat (so they can |
448 | 625 | # be matched and verified), and keeping the last_auth and expires from | 664 | # be matched and verified), and keeping the last_auth and expires from |
449 | 626 | # the original discharge macaroon | 665 | # the original discharge macaroon |
453 | 627 | d = _build_discharge(caveat_info['3rdparty'], raw_caveat_id, account, | 666 | d = _build_discharge( |
454 | 628 | last_auth=info_holder['last_auth'], | 667 | caveat_info['3rdparty'], raw_caveat_id, account, |
455 | 629 | expires=info_holder['expires']) | 668 | last_auth=macaroon_info['last_auth'], expires=macaroon_info['expires']) |
456 | 630 | 669 | ||
457 | 631 | # return the properly prepared discharge macaroon | 670 | # return the properly prepared discharge macaroon |
458 | 632 | discharge = root_macaroon.prepare_for_request(d) | 671 | discharge = root_macaroon.prepare_for_request(d) |
459 | 633 | return discharge | 672 | return discharge |
460 | 673 | |||
461 | 674 | |||
462 | 675 | def refresh_discharge(raw_caveat_id, discharge_macaroon_raw): | ||
463 | 676 | """Refresh a discharge with a new discharge macaroon.""" | ||
464 | 677 | try: | ||
465 | 678 | discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw) | ||
466 | 679 | except: | ||
467 | 680 | raise ValidationError("The received Macaroons are corrupt") | ||
468 | 681 | |||
469 | 682 | # get the decrypted deserialized info | ||
470 | 683 | caveat_info = _decrypt_caveat(raw_caveat_id) | ||
471 | 684 | account, macaroon_info = _account_from_macaroon( | ||
472 | 685 | discharge_macaroon, caveat_info['3rdparty']) | ||
473 | 686 | |||
474 | 687 | # create the new discharge macaroon with same location, key and | ||
475 | 688 | # identifier than it's original 3rd-party caveat (so they can | ||
476 | 689 | # be matched and verified), and keeping the last_auth and expires from | ||
477 | 690 | # the original discharge macaroon | ||
478 | 691 | discharge = _build_discharge( | ||
479 | 692 | caveat_info['3rdparty'], raw_caveat_id, account, | ||
480 | 693 | last_auth=macaroon_info['last_auth'], expires=macaroon_info['expires']) | ||
481 | 694 | |||
482 | 695 | # return the unbound discharge macaroon | ||
483 | 696 | return discharge | ||
484 | 634 | 697 | ||
485 | === modified file 'src/identityprovider/tests/test_auth.py' | |||
486 | --- src/identityprovider/tests/test_auth.py 2016-04-01 19:35:14 +0000 | |||
487 | +++ src/identityprovider/tests/test_auth.py 2016-05-05 12:45:29 +0000 | |||
488 | @@ -29,9 +29,11 @@ | |||
489 | 29 | LaunchpadBackend, | 29 | LaunchpadBackend, |
490 | 30 | SSOOAuthAuthentication, | 30 | SSOOAuthAuthentication, |
491 | 31 | SSORequestValidator, | 31 | SSORequestValidator, |
493 | 32 | _get_own_caveat, | 32 | _decrypt_caveat, |
494 | 33 | basic_authenticate, | 33 | basic_authenticate, |
495 | 34 | build_discharge_macaroon, | 34 | build_discharge_macaroon, |
496 | 35 | build_discharge_macaroon_from_root, | ||
497 | 36 | refresh_discharge, | ||
498 | 35 | refresh_macaroons, | 37 | refresh_macaroons, |
499 | 36 | validate_oauth_signature, | 38 | validate_oauth_signature, |
500 | 37 | ) | 39 | ) |
501 | @@ -958,26 +960,31 @@ | |||
502 | 958 | timer=self.dummy_timer) | 960 | timer=self.dummy_timer) |
503 | 959 | 961 | ||
504 | 960 | 962 | ||
506 | 961 | class BuildMacaroonDischargeTestCase(SSOBaseTestCase): | 963 | class BuildMacaroonFromRootDischargeTestCase(SSOBaseTestCase): |
507 | 964 | """Test the deprecated build_discharge_macaroon_from_root. | ||
508 | 965 | |||
509 | 966 | These tests are kept separated as that function won't evolve no more. | ||
510 | 967 | """ | ||
511 | 962 | 968 | ||
512 | 963 | def setUp(self): | 969 | def setUp(self): |
515 | 964 | super(BuildMacaroonDischargeTestCase, self).setUp() | 970 | super(BuildMacaroonFromRootDischargeTestCase, self).setUp() |
516 | 965 | self.root_macaroon, self.macaroon_random_key = self.build_macaroon() | 971 | self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon() |
517 | 966 | 972 | ||
518 | 967 | def test_root_macaroon_corrupt(self): | 973 | def test_root_macaroon_corrupt(self): |
520 | 968 | self.assertRaises(ValidationError, build_discharge_macaroon, | 974 | self.assertRaises(ValidationError, build_discharge_macaroon_from_root, |
521 | 969 | "fake account", "I'm a seriously corrupted macaroon") | 975 | "fake account", "I'm a seriously corrupted macaroon") |
522 | 970 | 976 | ||
523 | 971 | def test_root_macaroon_not_for_sso(self): | 977 | def test_root_macaroon_not_for_sso(self): |
526 | 972 | macaroon, _ = self.build_macaroon(service_location="other service") | 978 | macaroon, _, _ = self.build_macaroon(service_location="other service") |
527 | 973 | self.assertRaises(AuthenticationError, build_discharge_macaroon, | 979 | self.assertRaises(AuthenticationError, |
528 | 980 | build_discharge_macaroon_from_root, | ||
529 | 974 | "fake account", macaroon.serialize()) | 981 | "fake account", macaroon.serialize()) |
530 | 975 | 982 | ||
531 | 976 | def test_proper_discharging(self): | 983 | def test_proper_discharging(self): |
532 | 977 | # build the input and call | 984 | # build the input and call |
533 | 978 | real_account = self.factory.make_account() | 985 | real_account = self.factory.make_account() |
534 | 979 | before = now() | 986 | before = now() |
536 | 980 | discharge_macaroon = build_discharge_macaroon( | 987 | discharge_macaroon = build_discharge_macaroon_from_root( |
537 | 981 | real_account, self.root_macaroon.serialize()) | 988 | real_account, self.root_macaroon.serialize()) |
538 | 982 | after = now() | 989 | after = now() |
539 | 983 | 990 | ||
540 | @@ -1029,6 +1036,76 @@ | |||
541 | 1029 | [discharge_macaroon]) | 1036 | [discharge_macaroon]) |
542 | 1030 | 1037 | ||
543 | 1031 | 1038 | ||
544 | 1039 | class BuildMacaroonDischargeTestCase(SSOBaseTestCase): | ||
545 | 1040 | |||
546 | 1041 | def test_caveat_id_corrupt(self): | ||
547 | 1042 | self.assertRaises(ValidationError, build_discharge_macaroon, | ||
548 | 1043 | "fake account", "I'm a seriously corrupted caveatid") | ||
549 | 1044 | |||
550 | 1045 | def test_caveat_id_not_for_sso(self): | ||
551 | 1046 | macaroon, _, _ = self.build_macaroon(service_location="other service") | ||
552 | 1047 | (sso_caveat,) = [c for c in macaroon.third_party_caveats() | ||
553 | 1048 | if c.location == "other service"] | ||
554 | 1049 | |||
555 | 1050 | self.assertRaises(ValidationError, build_discharge_macaroon, | ||
556 | 1051 | "fake account", sso_caveat) | ||
557 | 1052 | |||
558 | 1053 | def test_proper_discharging(self): | ||
559 | 1054 | # build the input and call | ||
560 | 1055 | root_macaroon, _, random_key = self.build_macaroon() | ||
561 | 1056 | (sso_caveat,) = [c for c in root_macaroon.third_party_caveats() | ||
562 | 1057 | if c.location == settings.MACAROON_SERVICE_LOCATION] | ||
563 | 1058 | |||
564 | 1059 | real_account = self.factory.make_account() | ||
565 | 1060 | before = now() | ||
566 | 1061 | discharge_macaroon = build_discharge_macaroon( | ||
567 | 1062 | real_account, sso_caveat.caveat_id) | ||
568 | 1063 | after = now() | ||
569 | 1064 | |||
570 | 1065 | # test | ||
571 | 1066 | def checker(caveat): | ||
572 | 1067 | """Assure all caveats inside the discharged macaroon are ok.""" | ||
573 | 1068 | source, key, value = caveat.split("|", 2) | ||
574 | 1069 | |||
575 | 1070 | if key == 'valid_since': | ||
576 | 1071 | valid_since = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f') | ||
577 | 1072 | self.assertGreater(valid_since, before) | ||
578 | 1073 | self.assertGreater(after, valid_since) | ||
579 | 1074 | return True | ||
580 | 1075 | |||
581 | 1076 | if key == 'last_auth': | ||
582 | 1077 | last_auth = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f') | ||
583 | 1078 | self.assertGreater(last_auth, before) | ||
584 | 1079 | self.assertGreater(after, last_auth) | ||
585 | 1080 | return True | ||
586 | 1081 | |||
587 | 1082 | if key == 'account': | ||
588 | 1083 | acc = json.loads(base64.b64decode(value).decode("utf8")) | ||
589 | 1084 | self.assertEqual(acc['openid'], real_account.openid_identifier) | ||
590 | 1085 | self.assertEqual(acc['email'], | ||
591 | 1086 | real_account.preferredemail.email) | ||
592 | 1087 | self.assertEqual(acc['displayname'], real_account.displayname) | ||
593 | 1088 | self.assertEqual(acc['is_verified'], real_account.is_verified) | ||
594 | 1089 | return True | ||
595 | 1090 | |||
596 | 1091 | if key == 'expires': | ||
597 | 1092 | expires = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f') | ||
598 | 1093 | before_plus_ttl = before + timedelta( | ||
599 | 1094 | seconds=settings.MACAROON_TTL) | ||
600 | 1095 | after_plus_ttl = after + timedelta( | ||
601 | 1096 | seconds=settings.MACAROON_TTL) | ||
602 | 1097 | self.assertGreater(expires, before_plus_ttl) | ||
603 | 1098 | self.assertGreater(after_plus_ttl, expires) | ||
604 | 1099 | return True | ||
605 | 1100 | |||
606 | 1101 | # we're not validating an SSO from the discharged macaroon, fail! | ||
607 | 1102 | return False | ||
608 | 1103 | |||
609 | 1104 | v = Verifier() | ||
610 | 1105 | v.satisfy_general(checker) | ||
611 | 1106 | v.verify(discharge_macaroon, random_key, []) | ||
612 | 1107 | |||
613 | 1108 | |||
614 | 1032 | class MacaroonHelpersTestCase(SSOBaseTestCase): | 1109 | class MacaroonHelpersTestCase(SSOBaseTestCase): |
615 | 1033 | 1110 | ||
616 | 1034 | def test_get_caveat_ok(self): | 1111 | def test_get_caveat_ok(self): |
617 | @@ -1052,42 +1129,29 @@ | |||
618 | 1052 | settings.MACAROON_SERVICE_LOCATION, random_key, info_encrypted) | 1129 | settings.MACAROON_SERVICE_LOCATION, random_key, info_encrypted) |
619 | 1053 | 1130 | ||
620 | 1054 | # check | 1131 | # check |
623 | 1055 | raw_caveat_id, caveat_info = _get_own_caveat(root_macaroon) | 1132 | (sso_caveat,) = [c for c in root_macaroon.third_party_caveats() |
624 | 1056 | self.assertEqual(raw_caveat_id, info_encrypted) | 1133 | if c.location == settings.MACAROON_SERVICE_LOCATION] |
625 | 1134 | caveat_info = _decrypt_caveat(sso_caveat.caveat_id) | ||
626 | 1057 | self.assertEqual(caveat_info, info) | 1135 | self.assertEqual(caveat_info, info) |
627 | 1058 | 1136 | ||
628 | 1059 | def test_get_caveat_not_for_sso(self): | ||
629 | 1060 | macaroon, _ = self.build_macaroon(service_location="other service") | ||
630 | 1061 | self.assertRaises(AuthenticationError, _get_own_caveat, macaroon) | ||
631 | 1062 | |||
632 | 1063 | def test_get_caveat_badly_encrypted(self): | 1137 | def test_get_caveat_badly_encrypted(self): |
652 | 1064 | test_rsa_priv_key, test_rsa_pub_key = self.setup_key_pair() | 1138 | self.assertRaises(ValidationError, |
653 | 1065 | 1139 | _decrypt_caveat, b"not really well encrypted stuff") | |
654 | 1066 | # create a Macaron with the proper third party caveat | 1140 | |
655 | 1067 | macaroon_random_key = binascii.hexlify(os.urandom(32)) | 1141 | |
656 | 1068 | root_macaroon = Macaroon( | 1142 | class MacaroonRefreshFromRootTestCase(SSOBaseTestCase): |
657 | 1069 | location='The store ;)', | 1143 | """Test the deprecated refresh_macaroons. |
658 | 1070 | key=macaroon_random_key, | 1144 | |
659 | 1071 | identifier='A test macaroon', | 1145 | These tests are kept separated as that function won't evolve no more. |
660 | 1072 | ) | 1146 | """ |
642 | 1073 | random_key = binascii.hexlify(os.urandom(32)) | ||
643 | 1074 | root_macaroon.add_third_party_caveat( | ||
644 | 1075 | settings.MACAROON_SERVICE_LOCATION, random_key, | ||
645 | 1076 | b"not really well encrypted stuff") | ||
646 | 1077 | |||
647 | 1078 | # check | ||
648 | 1079 | self.assertRaises(AuthenticationError, _get_own_caveat, root_macaroon) | ||
649 | 1080 | |||
650 | 1081 | |||
651 | 1082 | class MacaroonRefreshTestCase(SSOBaseTestCase): | ||
661 | 1083 | 1147 | ||
662 | 1084 | def setUp(self): | 1148 | def setUp(self): |
665 | 1085 | super(MacaroonRefreshTestCase, self).setUp() | 1149 | super(MacaroonRefreshFromRootTestCase, self).setUp() |
666 | 1086 | self.root_macaroon, self.macaroon_random_key = self.build_macaroon() | 1150 | self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon() |
667 | 1087 | 1151 | ||
668 | 1088 | # discharge the test macaroon | 1152 | # discharge the test macaroon |
669 | 1089 | self.account = self.factory.make_account() | 1153 | self.account = self.factory.make_account() |
671 | 1090 | self.discharge_macaroon = build_discharge_macaroon( | 1154 | self.discharge_macaroon = build_discharge_macaroon_from_root( |
672 | 1091 | self.account, self.root_macaroon.serialize()) | 1155 | self.account, self.root_macaroon.serialize()) |
673 | 1092 | 1156 | ||
674 | 1093 | def test_root_macaroon_corrupt(self): | 1157 | def test_root_macaroon_corrupt(self): |
675 | @@ -1102,8 +1166,8 @@ | |||
676 | 1102 | 1166 | ||
677 | 1103 | def test_macaroons_dont_verify_ok(self): | 1167 | def test_macaroons_dont_verify_ok(self): |
678 | 1104 | # just get *another* discharge so it's not for the same root macaroon | 1168 | # just get *another* discharge so it's not for the same root macaroon |
681 | 1105 | other_root, _ = self.build_macaroon() | 1169 | other_root, _, _ = self.build_macaroon() |
682 | 1106 | other_discharge = build_discharge_macaroon( | 1170 | other_discharge = build_discharge_macaroon_from_root( |
683 | 1107 | self.account, other_root.serialize()) | 1171 | self.account, other_root.serialize()) |
684 | 1108 | self.assertRaises(AuthenticationError, refresh_macaroons, | 1172 | self.assertRaises(AuthenticationError, refresh_macaroons, |
685 | 1109 | self.root_macaroon.serialize(), | 1173 | self.root_macaroon.serialize(), |
686 | @@ -1197,3 +1261,120 @@ | |||
687 | 1197 | v.satisfy_general(checker) | 1261 | v.satisfy_general(checker) |
688 | 1198 | v.verify(self.root_macaroon, self.macaroon_random_key, | 1262 | v.verify(self.root_macaroon, self.macaroon_random_key, |
689 | 1199 | [new_discharge]) | 1263 | [new_discharge]) |
690 | 1264 | |||
691 | 1265 | |||
692 | 1266 | class MacaroonRefreshTestCase(SSOBaseTestCase): | ||
693 | 1267 | |||
694 | 1268 | def setUp(self): | ||
695 | 1269 | super(MacaroonRefreshTestCase, self).setUp() | ||
696 | 1270 | root_macaroon, _, self.random_key = self.build_macaroon() | ||
697 | 1271 | (caveat,) = [c for c in root_macaroon.third_party_caveats() | ||
698 | 1272 | if c.location == settings.MACAROON_SERVICE_LOCATION] | ||
699 | 1273 | self.caveat_id = caveat.caveat_id | ||
700 | 1274 | |||
701 | 1275 | # get a discharge for the test macaroon | ||
702 | 1276 | self.account = self.factory.make_account() | ||
703 | 1277 | self.discharge_macaroon = build_discharge_macaroon( | ||
704 | 1278 | self.account, self.caveat_id) | ||
705 | 1279 | |||
706 | 1280 | def test_caveat_id_corrupt(self): | ||
707 | 1281 | self.assertRaises( | ||
708 | 1282 | ValidationError, refresh_discharge, | ||
709 | 1283 | "Corrupted caveat id", self.discharge_macaroon.serialize()) | ||
710 | 1284 | |||
711 | 1285 | def test_discharge_macaroon_corrupt(self): | ||
712 | 1286 | self.assertRaises( | ||
713 | 1287 | ValidationError, refresh_discharge, | ||
714 | 1288 | self.caveat_id, "Seriously corrupted macaroon") | ||
715 | 1289 | |||
716 | 1290 | def test_macaroons_dont_verify_ok(self): | ||
717 | 1291 | # just get *another* discharge so it's not for the same root macaroon | ||
718 | 1292 | other_root, _, _ = self.build_macaroon() | ||
719 | 1293 | (other_caveat,) = [c for c in other_root.third_party_caveats() | ||
720 | 1294 | if c.location == settings.MACAROON_SERVICE_LOCATION] | ||
721 | 1295 | other_discharge = build_discharge_macaroon( | ||
722 | 1296 | self.account, other_caveat.caveat_id) | ||
723 | 1297 | self.assertRaises(AuthenticationError, refresh_discharge, | ||
724 | 1298 | self.caveat_id, other_discharge.serialize()) | ||
725 | 1299 | |||
726 | 1300 | def test_deactivated_account(self): | ||
727 | 1301 | self.account.deactivate() | ||
728 | 1302 | self.assertRaises(AccountDeactivated, refresh_discharge, | ||
729 | 1303 | self.caveat_id, self.discharge_macaroon.serialize()) | ||
730 | 1304 | |||
731 | 1305 | def test_password_changed(self): | ||
732 | 1306 | self.account.set_password("a new password") | ||
733 | 1307 | self.assertRaises(AuthenticationError, refresh_discharge, | ||
734 | 1308 | self.caveat_id, self.discharge_macaroon.serialize()) | ||
735 | 1309 | |||
736 | 1310 | def test_password_with_no_datetime(self): | ||
737 | 1311 | # simulate an "old" account password (before we started to keep the | ||
738 | 1312 | # changed date); note that I'm bypassing the AccountPassword save(), | ||
739 | 1313 | # which will update the attribute that I want in Null! (and we confirm | ||
740 | 1314 | # that in the assert) | ||
741 | 1315 | self.account.accountpassword.date_changed = None | ||
742 | 1316 | super(AccountPassword, self.account.accountpassword).save() | ||
743 | 1317 | assert self.account.accountpassword.date_changed is None | ||
744 | 1318 | |||
745 | 1319 | # check that auths ok | ||
746 | 1320 | refresh_discharge(self.caveat_id, self.discharge_macaroon.serialize()) | ||
747 | 1321 | |||
748 | 1322 | def test_proper_refreshing(self): | ||
749 | 1323 | old_discharge = self.discharge_macaroon # just rename for readability | ||
750 | 1324 | service_location = settings.MACAROON_SERVICE_LOCATION | ||
751 | 1325 | |||
752 | 1326 | def get_value(search_key): | ||
753 | 1327 | for caveat in old_discharge.first_party_caveats(): | ||
754 | 1328 | source, key, value = caveat.caveat_id.split("|", 2) | ||
755 | 1329 | if source == service_location and key == search_key: | ||
756 | 1330 | return value | ||
757 | 1331 | |||
758 | 1332 | # get old values from the macaroon and also change the account to see | ||
759 | 1333 | # that reflected | ||
760 | 1334 | old_last_auth = get_value('last_auth') | ||
761 | 1335 | old_expires = get_value('expires') | ||
762 | 1336 | new_mail = self.factory.make_email_for_account( | ||
763 | 1337 | self.account, status=EmailStatus.PREFERRED) | ||
764 | 1338 | self.account.displayname = "New test display name" | ||
765 | 1339 | self.account.save() | ||
766 | 1340 | |||
767 | 1341 | # call! | ||
768 | 1342 | before = now() | ||
769 | 1343 | new_discharge = refresh_discharge(self.caveat_id, | ||
770 | 1344 | old_discharge.serialize()) | ||
771 | 1345 | after = now() | ||
772 | 1346 | |||
773 | 1347 | # test | ||
774 | 1348 | def checker(caveat): | ||
775 | 1349 | """Assure all caveats inside the discharged macaroon are ok.""" | ||
776 | 1350 | source, key, value = caveat.split("|", 2) | ||
777 | 1351 | |||
778 | 1352 | if key == 'valid_since': | ||
779 | 1353 | valid_since = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f') | ||
780 | 1354 | self.assertGreater(valid_since, before) | ||
781 | 1355 | self.assertGreater(after, valid_since) | ||
782 | 1356 | return True | ||
783 | 1357 | |||
784 | 1358 | if key == 'last_auth': | ||
785 | 1359 | self.assertEqual(value, old_last_auth) | ||
786 | 1360 | return True | ||
787 | 1361 | |||
788 | 1362 | if key == 'account': | ||
789 | 1363 | acc = json.loads(base64.b64decode(value).decode("utf8")) | ||
790 | 1364 | self.assertEqual(acc['openid'], self.account.openid_identifier) | ||
791 | 1365 | self.assertEqual(acc['email'], new_mail.email) | ||
792 | 1366 | self.assertEqual(acc['displayname'], "New test display name") | ||
793 | 1367 | self.assertEqual(acc['is_verified'], self.account.is_verified) | ||
794 | 1368 | return True | ||
795 | 1369 | |||
796 | 1370 | if key == 'expires': | ||
797 | 1371 | self.assertEqual(value, old_expires) | ||
798 | 1372 | return True | ||
799 | 1373 | |||
800 | 1374 | # we're not validating an SSO from the discharged macaroon, fail! | ||
801 | 1375 | return False | ||
802 | 1376 | |||
803 | 1377 | # verify using the NEW discharge macaroon | ||
804 | 1378 | v = Verifier() | ||
805 | 1379 | v.satisfy_general(checker) | ||
806 | 1380 | v.verify(new_discharge, self.random_key, []) | ||
807 | 1200 | 1381 | ||
808 | === modified file 'src/identityprovider/tests/test_forms.py' | |||
809 | --- src/identityprovider/tests/test_forms.py 2016-04-11 11:57:38 +0000 | |||
810 | +++ src/identityprovider/tests/test_forms.py 2016-05-05 12:45:29 +0000 | |||
811 | @@ -1138,7 +1138,7 @@ | |||
812 | 1138 | """The server always returns discharge macaroons to trusted sites, | 1138 | """The server always returns discharge macaroons to trusted sites, |
813 | 1139 | regardless of the state of the checkbox in the UI. | 1139 | regardless of the state of the checkbox in the UI. |
814 | 1140 | """ | 1140 | """ |
816 | 1141 | root_macaroon, _ = self.build_macaroon() | 1141 | root_macaroon, _, _ = self.build_macaroon() |
817 | 1142 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) | 1142 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) |
818 | 1143 | form = MacaroonRequestForm( | 1143 | form = MacaroonRequestForm( |
819 | 1144 | request=self._get_request_with_post_args(), | 1144 | request=self._get_request_with_post_args(), |
820 | @@ -1149,7 +1149,7 @@ | |||
821 | 1149 | """The server returns discharge macaroons to untrusted sites when | 1149 | """The server returns discharge macaroons to untrusted sites when |
822 | 1150 | the user checks the checkbox in the UI. | 1150 | the user checks the checkbox in the UI. |
823 | 1151 | """ | 1151 | """ |
825 | 1152 | root_macaroon, _ = self.build_macaroon() | 1152 | root_macaroon, _, _ = self.build_macaroon() |
826 | 1153 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) | 1153 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) |
827 | 1154 | form = MacaroonRequestForm( | 1154 | form = MacaroonRequestForm( |
828 | 1155 | request=self._get_request_with_post_args(macaroon='macaroon'), | 1155 | request=self._get_request_with_post_args(macaroon='macaroon'), |
829 | @@ -1160,7 +1160,7 @@ | |||
830 | 1160 | """The server does not return discharge macaroons to untrusted sites | 1160 | """The server does not return discharge macaroons to untrusted sites |
831 | 1161 | when the user does not check the checkbox in the UI. | 1161 | when the user does not check the checkbox in the UI. |
832 | 1162 | """ | 1162 | """ |
834 | 1163 | root_macaroon, _ = self.build_macaroon() | 1163 | root_macaroon, _, _ = self.build_macaroon() |
835 | 1164 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) | 1164 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) |
836 | 1165 | form = MacaroonRequestForm( | 1165 | form = MacaroonRequestForm( |
837 | 1166 | request=self._get_request_with_post_args(), | 1166 | request=self._get_request_with_post_args(), |
838 | @@ -1169,7 +1169,7 @@ | |||
839 | 1169 | 1169 | ||
840 | 1170 | def test_checkbox_status_for_trusted_site(self): | 1170 | def test_checkbox_status_for_trusted_site(self): |
841 | 1171 | """Checkboxes are always checked if the site is trusted.""" | 1171 | """Checkboxes are always checked if the site is trusted.""" |
843 | 1172 | root_macaroon, _ = self.build_macaroon() | 1172 | root_macaroon, _, _ = self.build_macaroon() |
844 | 1173 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) | 1173 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) |
845 | 1174 | form = MacaroonRequestForm( | 1174 | form = MacaroonRequestForm( |
846 | 1175 | request=self._get_request_with_post_args(), | 1175 | request=self._get_request_with_post_args(), |
847 | @@ -1178,7 +1178,7 @@ | |||
848 | 1178 | 1178 | ||
849 | 1179 | def test_checkbox_status_for_untrusted_site(self): | 1179 | def test_checkbox_status_for_untrusted_site(self): |
850 | 1180 | """Checkboxes are checked by default if the site is untrusted.""" | 1180 | """Checkboxes are checked by default if the site is untrusted.""" |
852 | 1181 | root_macaroon, _ = self.build_macaroon() | 1181 | root_macaroon, _, _ = self.build_macaroon() |
853 | 1182 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) | 1182 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) |
854 | 1183 | form = MacaroonRequestForm( | 1183 | form = MacaroonRequestForm( |
855 | 1184 | request=self._get_request_with_post_args(), | 1184 | request=self._get_request_with_post_args(), |
856 | @@ -1189,7 +1189,7 @@ | |||
857 | 1189 | """Checkboxes respect user preferences on untrusted sites where | 1189 | """Checkboxes respect user preferences on untrusted sites where |
858 | 1190 | available. | 1190 | available. |
859 | 1191 | """ | 1191 | """ |
861 | 1192 | root_macaroon, _ = self.build_macaroon() | 1192 | root_macaroon, _, _ = self.build_macaroon() |
862 | 1193 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) | 1193 | macaroon_request = MacaroonRequest(root_macaroon.serialize()) |
863 | 1194 | approved_data = { | 1194 | approved_data = { |
864 | 1195 | 'requested': ['macaroon'], | 1195 | 'requested': ['macaroon'], |
865 | 1196 | 1196 | ||
866 | === modified file 'src/identityprovider/tests/test_macaroon.py' | |||
867 | --- src/identityprovider/tests/test_macaroon.py 2016-04-05 22:12:47 +0000 | |||
868 | +++ src/identityprovider/tests/test_macaroon.py 2016-05-05 12:45:29 +0000 | |||
869 | @@ -45,7 +45,7 @@ | |||
870 | 45 | def setUp(self): | 45 | def setUp(self): |
871 | 46 | super(MacaroonRequestTestCase, self).setUp() | 46 | super(MacaroonRequestTestCase, self).setUp() |
872 | 47 | 47 | ||
874 | 48 | self.root_macaroon, self.macaroon_random_key = self.build_macaroon() | 48 | self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon() |
875 | 49 | self.req = MacaroonRequest(self.root_macaroon.serialize()) | 49 | self.req = MacaroonRequest(self.root_macaroon.serialize()) |
876 | 50 | 50 | ||
877 | 51 | def assertMacaroonsEqual(self, expected, observed): | 51 | def assertMacaroonsEqual(self, expected, observed): |
878 | 52 | 52 | ||
879 | === modified file 'src/identityprovider/tests/test_views_server.py' | |||
880 | --- src/identityprovider/tests/test_views_server.py 2016-04-04 15:41:06 +0000 | |||
881 | +++ src/identityprovider/tests/test_views_server.py 2016-05-05 12:45:29 +0000 | |||
882 | @@ -227,7 +227,7 @@ | |||
883 | 227 | self._test_auto_auth(sreg=['fullname']) | 227 | self._test_auto_auth(sreg=['fullname']) |
884 | 228 | 228 | ||
885 | 229 | def test_handle_user_response_auto_auth_discharge_macaroon(self): | 229 | def test_handle_user_response_auto_auth_discharge_macaroon(self): |
887 | 230 | root_macaroon, macaroon_random_key = self.build_macaroon() | 230 | root_macaroon, macaroon_random_key, _ = self.build_macaroon() |
888 | 231 | # Add padding to force a POST after signing. We don't know exactly | 231 | # Add padding to force a POST after signing. We don't know exactly |
889 | 232 | # how long the serialized discharge macaroon will be yet, but it | 232 | # how long the serialized discharge macaroon will be yet, but it |
890 | 233 | # will probably be at least 1024 bytes. | 233 | # will probably be at least 1024 bytes. |
891 | @@ -1065,7 +1065,7 @@ | |||
892 | 1065 | # make sure rpconfig is set to auto authorize | 1065 | # make sure rpconfig is set to auto authorize |
893 | 1066 | OpenIDRPConfig.objects.create( | 1066 | OpenIDRPConfig.objects.create( |
894 | 1067 | trust_root='http://localhost/', auto_authorize=True) | 1067 | trust_root='http://localhost/', auto_authorize=True) |
896 | 1068 | root_macaroon, macaroon_random_key = self.build_macaroon() | 1068 | root_macaroon, macaroon_random_key, _ = self.build_macaroon() |
897 | 1069 | param_overrides = { | 1069 | param_overrides = { |
898 | 1070 | 'openid.ns.macaroon': MACAROON_NS, | 1070 | 'openid.ns.macaroon': MACAROON_NS, |
899 | 1071 | 'openid.macaroon.root': root_macaroon.serialize(), | 1071 | 'openid.macaroon.root': root_macaroon.serialize(), |
900 | @@ -1089,7 +1089,7 @@ | |||
901 | 1089 | root_macaroon, macaroon_random_key, [discharge_macaroon])) | 1089 | root_macaroon, macaroon_random_key, [discharge_macaroon])) |
902 | 1090 | 1090 | ||
903 | 1091 | def test_state_of_checkboxes_and_data_formats_macaroon(self): | 1091 | def test_state_of_checkboxes_and_data_formats_macaroon(self): |
905 | 1092 | root_macaroon, _ = self.build_macaroon() | 1092 | root_macaroon, _, _ = self.build_macaroon() |
906 | 1093 | param_overrides = { | 1093 | param_overrides = { |
907 | 1094 | 'openid.ns.macaroon': MACAROON_NS, | 1094 | 'openid.ns.macaroon': MACAROON_NS, |
908 | 1095 | 'openid.macaroon.root': root_macaroon.serialize(), | 1095 | 'openid.macaroon.root': root_macaroon.serialize(), |
909 | @@ -1864,7 +1864,7 @@ | |||
910 | 1864 | if with_teams: | 1864 | if with_teams: |
911 | 1865 | params['openid.lp.query_membership'] = 'ubuntu-team' | 1865 | params['openid.lp.query_membership'] = 'ubuntu-team' |
912 | 1866 | if with_macaroon: | 1866 | if with_macaroon: |
914 | 1867 | root_macaroon, _ = self.build_macaroon() | 1867 | root_macaroon, _, _ = self.build_macaroon() |
915 | 1868 | params['openid.ns.macaroon'] = MACAROON_NS | 1868 | params['openid.ns.macaroon'] = MACAROON_NS |
916 | 1869 | params['openid.macaroon.root'] = root_macaroon.serialize() | 1869 | params['openid.macaroon.root'] = root_macaroon.serialize() |
917 | 1870 | provider_url = get_provider_url(request) | 1870 | provider_url = get_provider_url(request) |
918 | 1871 | 1871 | ||
919 | === modified file 'src/identityprovider/tests/utils.py' | |||
920 | --- src/identityprovider/tests/utils.py 2016-04-28 13:01:53 +0000 | |||
921 | +++ src/identityprovider/tests/utils.py 2016-05-05 12:45:29 +0000 | |||
922 | @@ -202,7 +202,7 @@ | |||
923 | 202 | test_rsa_pub_key.encrypt(json.dumps(info), 32)[0]) | 202 | test_rsa_pub_key.encrypt(json.dumps(info), 32)[0]) |
924 | 203 | root_macaroon.add_third_party_caveat( | 203 | root_macaroon.add_third_party_caveat( |
925 | 204 | service_location, random_key, info_encrypted) | 204 | service_location, random_key, info_encrypted) |
927 | 205 | return root_macaroon, macaroon_random_key | 205 | return root_macaroon, macaroon_random_key, random_key |
928 | 206 | 206 | ||
929 | 207 | 207 | ||
930 | 208 | class SSOBaseTestCase(SSOBaseTestCaseMixin, TestCase): | 208 | class SSOBaseTestCase(SSOBaseTestCaseMixin, TestCase): |
931 | 209 | 209 | ||
932 | === modified file 'src/identityprovider/views/server.py' | |||
933 | --- src/identityprovider/views/server.py 2016-04-29 14:17:15 +0000 | |||
934 | +++ src/identityprovider/views/server.py 2016-05-05 12:45:29 +0000 | |||
935 | @@ -42,7 +42,7 @@ | |||
936 | 42 | from openid.yadis.constants import YADIS_HEADER_NAME | 42 | from openid.yadis.constants import YADIS_HEADER_NAME |
937 | 43 | 43 | ||
938 | 44 | from identityprovider import signed | 44 | from identityprovider import signed |
940 | 45 | from identityprovider.auth import build_discharge_macaroon | 45 | from identityprovider.auth import build_discharge_macaroon_from_root |
941 | 46 | from identityprovider.const import ( | 46 | from identityprovider.const import ( |
942 | 47 | AX_DATA_FIELDS, | 47 | AX_DATA_FIELDS, |
943 | 48 | MACAROON_NS, | 48 | MACAROON_NS, |
944 | @@ -769,7 +769,7 @@ | |||
945 | 769 | rpconfig = utils.get_rpconfig(openid_request.trust_root) | 769 | rpconfig = utils.get_rpconfig(openid_request.trust_root) |
946 | 770 | form = MacaroonRequestForm(request, macaroon_request, rpconfig) | 770 | form = MacaroonRequestForm(request, macaroon_request, rpconfig) |
947 | 771 | if form.data_approved_for_request: | 771 | if form.data_approved_for_request: |
949 | 772 | discharge_macaroon = build_discharge_macaroon( | 772 | discharge_macaroon = build_discharge_macaroon_from_root( |
950 | 773 | request.user, macaroon_request.root_macaroon_raw) | 773 | request.user, macaroon_request.root_macaroon_raw) |
951 | 774 | macaroon_response = MacaroonResponse.extractResponse( | 774 | macaroon_response = MacaroonResponse.extractResponse( |
952 | 775 | macaroon_request, discharge_macaroon.serialize()) | 775 | macaroon_request, discharge_macaroon.serialize()) |
LGTM