Merge lp:~roadmr/canonical-identity-provider/bulk-caveat-id-macaroon-discharge into lp:canonical-identity-provider/release

Proposed by Daniel Manrique
Status: Work in progress
Proposed branch: lp:~roadmr/canonical-identity-provider/bulk-caveat-id-macaroon-discharge
Merge into: lp:canonical-identity-provider/release
Diff against target: 185 lines (+127/-1)
3 files modified
docs/login.txt (+67/-0)
src/api/v20/handlers.py (+17/-1)
src/api/v20/tests/test_handlers.py (+43/-0)
To merge this branch: bzr merge lp:~roadmr/canonical-identity-provider/bulk-caveat-id-macaroon-discharge
Reviewer Review Type Date Requested Status
Ubuntu One hackers Pending
Review via email: mp+336506@code.launchpad.net

Description of the change

lalala

To post a comment you must log in.
1593. By Daniel Manrique

Test for mixed caveat_ids: a good one and a bogus, corrupty one

1594. By Daniel Manrique

Small simplification of test

1595. By Daniel Manrique

Documentation for the discharge endpoint explaining the single/multiple caveat_id flow.

Didn't document the old "root_macaroon(s)" API on purpose because it's deprecated and consumers
should NOT use it.

1596. By Daniel Manrique

Ahh, so that's what simple_macaroon is for

Unmerged revisions

1596. By Daniel Manrique

Ahh, so that's what simple_macaroon is for

1595. By Daniel Manrique

Documentation for the discharge endpoint explaining the single/multiple caveat_id flow.

Didn't document the old "root_macaroon(s)" API on purpose because it's deprecated and consumers
should NOT use it.

1594. By Daniel Manrique

Small simplification of test

1593. By Daniel Manrique

Test for mixed caveat_ids: a good one and a bogus, corrupty one

1592. By Daniel Manrique

Allow sending multiple caveat_ids to MacaroonDischargeHandler.

The semantics are similar to the old macaroons parameter (contrast with
macaroon). A new payload parameter caveat_ids is supported, should receive
a list of lists/tuples with numeric_id/caveat_id and should send back a
list of numeric_id/3rd_party_discharge (or a nice error) for each.

1591. By Daniel Manrique

Test for multiple 'good' caveat_id macaroon discharging

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/login.txt'
2--- docs/login.txt 2013-12-04 13:49:39 +0000
3+++ docs/login.txt 2018-01-26 19:53:09 +0000
4@@ -1,8 +1,16 @@
5 .. vim: ft=rst
6
7+=====
8 Login
9 =====
10
11+API-based logins return a token for the client to use in subsequent requests to
12+prove its identity and authorization. The token can be in one of two formats: either
13+an OAuth token or a discharge macaroon.
14+
15+OAuth
16+=====
17+
18 Login is performed by creating an :ref:`OAuth token <token_oauth>` resource.
19 Refer to :ref:`"Create an oauth token" <token_oauth-create>` for details.
20
21@@ -15,3 +23,62 @@
22 -H 'Content-Type: application/json' \
23 -X POST \
24 https://login.ubuntu.com/api/v2/tokens/oauth
25+
26+Macaroon
27+========
28+
29+In this case, logging in returns one or more discharge macaroons which satisfy
30+the given 3rd-party caveats. The macaroons can be presented along with the
31+corresponding root macaroon to complete requests in services which require SSO
32+authentication.
33+
34+Two ways of using this mechanism are supported:
35+
36+1. Provide a single extracted caveat_id with the "caveat_id" parameter. A
37+ single "discharge_macaroon" will be returned.
38+2. Provide a list of caveat_ids with the "caveat_ids" parameter. The list
39+ should consist of N sub-lists, each with two elements: an identifier and the
40+ corresponding serialized caveat_id. A "discharge_macaroons" list will be
41+ returned. It will contain N sub-lists, with the given identifiers and the
42+ corresponding discharge for each caveat_id. If any of the caveat_ids is
43+ invalid, a global "invalid-data" error code will be returned.
44+
45+
46+
47+Examples
48+--------
49+Single caveat_id request:
50+
51+.. code-block:: bash
52+
53+ curl -d '{"email": "foo@example.com", "password": "thepassword", "otp": "1234567","caveat_id":"serialized-caveat-id"}' \
54+ -H 'Content-Type: application/json' \
55+ -X POST \
56+ https://login.ubuntu.com/api/v2/tokens/discharge
57+
58+Response:
59+
60+.. sourcecode:: json
61+
62+ {
63+ "discharge_macaroon": "a-serialized-discharge-macaroon"
64+ }
65+
66+Multiple caveat_id request:
67+
68+.. code-block:: bash
69+
70+ curl -d '{"email": "foo@example.com", "password": "thepassword", "otp": "1234567","caveat_ids": [[0, "serialized-caveat-id-0"], [1, "serialized-caveat-id-1"]]}' \
71+ -H 'Content-Type: application/json' \
72+ -X POST \
73+ https://login.ubuntu.com/api/v2/tokens/discharge
74+
75+Response:
76+
77+.. sourcecode:: json
78+
79+ {
80+ "discharge_macaroons": [[0, "serialized-discharge-macaroon-0"], [1, "serialized-discharge-macaroon-1"]]
81+ }
82+
83+
84
85=== modified file 'src/api/v20/handlers.py'
86--- src/api/v20/handlers.py 2018-01-23 17:38:47 +0000
87+++ src/api/v20/handlers.py 2018-01-26 19:53:09 +0000
88@@ -501,7 +501,14 @@
89 simple_macaroon = True
90 root_macaroons_info = None
91 caveat_id = data['caveat_id']
92+ caveat_ids = None
93 stats.increment('macaroon.discharge', key='caveat_id')
94+ elif 'caveat_ids' in data:
95+ simple_macaroon = False
96+ root_macaroons_info = None
97+ caveat_id = None
98+ caveat_ids = data['caveat_ids']
99+ stats.increment('macaroon.discharge', key='multi_caveat_ids')
100 else:
101 # let's flag the new one only, not the deprecated ones
102 missing = {'caveat_id': [FIELD_REQUIRED]}
103@@ -545,9 +552,16 @@
104 return response
105
106 try:
107- if root_macaroons_info is None:
108+ if root_macaroons_info is None and caveat_id:
109 discharge = auth.build_discharge_macaroon(account, caveat_id)
110 all_discharges = [(None, discharge.serialize())]
111+ elif root_macaroons_info is None and caveat_ids:
112+ # Multiple
113+ all_discharges = []
114+ for mac_id, a_caveat_id in caveat_ids:
115+ discharge = auth.build_discharge_macaroon(
116+ account, a_caveat_id)
117+ all_discharges.append((mac_id, discharge.serialize()))
118 else:
119 # 2016-05-03: this is deprecated, just supporting it until
120 # no more clients send root macaroon(s)
121@@ -564,8 +578,10 @@
122
123 response = rc.ALL_OK
124 if simple_macaroon:
125+ # either caveat_id or macaroon
126 response.content = dict(discharge_macaroon=all_discharges[0][1])
127 else:
128+ # either caveat_ids or macaroons
129 response.content = dict(discharge_macaroons=all_discharges)
130
131 log_type = AuthLogType.API_MACAROON_DISCHARGE_NEW
132
133=== modified file 'src/api/v20/tests/test_handlers.py'
134--- src/api/v20/tests/test_handlers.py 2018-01-22 15:15:48 +0000
135+++ src/api/v20/tests/test_handlers.py 2018-01-26 19:53:09 +0000
136@@ -2784,6 +2784,49 @@
137 expected_status_code=400,
138 check_login_failed=False)
139
140+ def test_multiple_caveat_ids_discharges_created(self):
141+ # build *several* macaroons and send them all at once
142+ roots, rkeys, _ = zip(*[
143+ self.build_macaroon(version=1) for _ in range(3)])
144+ ids = range(3)
145+ roots = dict(zip(ids, roots))
146+ rkeys = dict(zip(ids, rkeys))
147+ caveat_ids = []
148+ for mac_id, m in roots.items():
149+ (caveat,) = [c for c in m.third_party_caveats()
150+ if c.location == settings.MACAROON_SERVICE_LOCATION]
151+ caveat_ids.append((mac_id, caveat.caveat_id))
152+ data = dict(email='foo@bar.com', password='foobar123',
153+ caveat_ids=caveat_ids)
154+ json_body = self.do_post(data=data)
155+
156+ # get all the discharges and verify their integrity
157+ verifying_function = lambda c: True
158+ for mac_id, discharge_macaroon_raw in json_body['discharge_macaroons']:
159+ discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw)
160+ root_macaroon = roots[mac_id]
161+ key = rkeys[mac_id]
162+ discharge_macaroon = root_macaroon.prepare_for_request(
163+ discharge_macaroon)
164+ v = Verifier()
165+ v.satisfy_general(verifying_function)
166+ v.verify(root_macaroon, key, [discharge_macaroon])
167+
168+ self.stats_increment.assert_any_call(
169+ 'macaroon.discharge', key='multi_caveat_ids')
170+
171+ def test_multiple_caveat_ids_mixed(self):
172+ good_macaroon, _, _ = self.build_macaroon(version=1)
173+ (good_caveat,) = [c for c in good_macaroon.third_party_caveats()
174+ if c.location == settings.MACAROON_SERVICE_LOCATION]
175+ caveat_ids = [(0, good_caveat.caveat_id),
176+ (1, "A bad caveat id")]
177+ data = dict(email='foo@bar.com', password='foobar123',
178+ caveat_ids=caveat_ids)
179+ self.assert_failed_login('INVALID_DATA', data,
180+ expected_status_code=400,
181+ check_login_failed=False)
182+
183
184 class MacaroonHandlerTimelineTestCase(MacaroonHandlerBaseTestCase,
185 TimelineActionMixin):