Merge lp:~twom/canonical-identity-provider/unexpiring-discharge-macaroon into lp:canonical-identity-provider/release

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: no longer in the source branch.
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~twom/canonical-identity-provider/unexpiring-discharge-macaroon
Merge into: lp:canonical-identity-provider/release
Diff against target: 225 lines (+95/-55)
3 files modified
django_project/settings_base.py (+0/-1)
src/identityprovider/auth.py (+4/-5)
src/identityprovider/tests/test_auth.py (+91/-49)
To merge this branch: bzr merge lp:~twom/canonical-identity-provider/unexpiring-discharge-macaroon
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+359240@code.launchpad.net

Commit message

Remove the expiry caveat from the discharge macaroon.

Description of the change

Expiry is now handled by the root macaroons of snapauth and SCA, so remove it from SSO. This allows the other services to control their own expiry.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

I think you should also replace the 'expires' tests in BuildMacaroonFromRootDischargeTestCase.test_proper_discharging and BuildMacaroonDischargeTestCase.test_proper_discharging with tests that the 'expires' caveat is absent (perhaps just "return False" in the checker function if you encounter one of those). When you've done that, it should also be possible to remove MACAROON_TTL from django_project/settings_base.py.

This can't be landed until the corresponding SCA and snapauth changes are on production, and I'd suggest waiting a week or two after that for safety.

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

Oh, and please make sure that MacaroonRefreshTestCase tests the cases of refreshing an otherwise-valid macaroon both with and without an 'expires' caveat.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'django_project/settings_base.py'
--- django_project/settings_base.py 2018-12-20 20:25:26 +0000
+++ django_project/settings_base.py 2019-01-14 10:41:34 +0000
@@ -372,7 +372,6 @@
372LP_API_CONSUMER_KEY = ''372LP_API_CONSUMER_KEY = ''
373LP_API_TOKEN = ''373LP_API_TOKEN = ''
374LP_API_TOKEN_SECRET = ''374LP_API_TOKEN_SECRET = ''
375MACAROON_TTL = 365 * 24 * 3600 # seconds
376MACAROON_SERVICE_LOCATION = HOSTNAME375MACAROON_SERVICE_LOCATION = HOSTNAME
377MANAGERS = []376MANAGERS = []
378MAX_EMAILS_PER_ACCOUNT = 30377MAX_EMAILS_PER_ACCOUNT = 30
379378
=== modified file 'src/identityprovider/auth.py'
--- src/identityprovider/auth.py 2018-02-21 13:46:00 +0000
+++ src/identityprovider/auth.py 2019-01-14 10:41:34 +0000
@@ -577,13 +577,11 @@
577 now_string = now().strftime(MACAROON_TIMESTAMP_FORMAT)577 now_string = now().strftime(MACAROON_TIMESTAMP_FORMAT)
578 if last_auth is None:578 if last_auth is None:
579 last_auth = now_string579 last_auth = now_string
580 if expires is None:
581 expires_ts = now() + datetime.timedelta(seconds=settings.MACAROON_TTL)
582 expires = expires_ts.strftime(MACAROON_TIMESTAMP_FORMAT)
583580
584 d.add_first_party_caveat(service_location + '|valid_since|' + now_string)581 d.add_first_party_caveat(service_location + '|valid_since|' + now_string)
585 d.add_first_party_caveat(service_location + '|last_auth|' + last_auth)582 d.add_first_party_caveat(service_location + '|last_auth|' + last_auth)
586 d.add_first_party_caveat(service_location + '|expires|' + expires)583 if expires is not None:
584 d.add_first_party_caveat(service_location + '|expires|' + expires)
587 return d585 return d
588586
589587
@@ -685,7 +683,8 @@
685 # the original discharge macaroon683 # the original discharge macaroon
686 discharge = _build_discharge(684 discharge = _build_discharge(
687 caveat_info['3rdparty'], discharge_macaroon.identifier, account,685 caveat_info['3rdparty'], discharge_macaroon.identifier, account,
688 last_auth=macaroon_info['last_auth'], expires=macaroon_info['expires'])686 last_auth=macaroon_info['last_auth'],
687 expires=macaroon_info.get('expires'))
689688
690 # return the unbound discharge macaroon689 # return the unbound discharge macaroon
691 return discharge690 return discharge
692691
=== modified file 'src/identityprovider/tests/test_auth.py'
--- src/identityprovider/tests/test_auth.py 2018-02-21 14:12:14 +0000
+++ src/identityprovider/tests/test_auth.py 2019-01-14 10:41:34 +0000
@@ -26,9 +26,11 @@
26from pymacaroons import Verifier26from pymacaroons import Verifier
2727
28from identityprovider.auth import (28from identityprovider.auth import (
29 MACAROON_TIMESTAMP_FORMAT,
29 LaunchpadBackend,30 LaunchpadBackend,
30 SSOOAuthAuthentication,31 SSOOAuthAuthentication,
31 SSORequestValidator,32 SSORequestValidator,
33 _build_discharge,
32 _decrypt_caveat,34 _decrypt_caveat,
33 basic_authenticate,35 basic_authenticate,
34 build_discharge_macaroon,36 build_discharge_macaroon,
@@ -1041,6 +1043,10 @@
1041 self.assertEqual(expected, actual)1043 self.assertEqual(expected, actual)
1042 return True1044 return True
10431045
1046 # Expires caveat no longer exists by default
1047 if key == 'expires':
1048 return False
1049
1044 if key == 'expires':1050 if key == 'expires':
1045 expires = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')1051 expires = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
1046 before_plus_ttl = before + timedelta(1052 before_plus_ttl = before + timedelta(
@@ -1206,9 +1212,49 @@
12061212
1207 # get a discharge for the test macaroon1213 # get a discharge for the test macaroon
1208 self.account = self.factory.make_account()1214 self.account = self.factory.make_account()
1215 self.caveat = caveat
1209 self.discharge_macaroon = build_discharge_macaroon(1216 self.discharge_macaroon = build_discharge_macaroon(
1210 self.account, caveat.caveat_id)1217 self.account, caveat.caveat_id)
12111218
1219 def checker(self, caveat):
1220 """Assure all caveats inside the discharged macaroon are ok."""
1221 source, key, value = caveat.split("|", 2)
1222
1223 if key == 'valid_since':
1224 valid_since = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
1225 self.assertGreater(valid_since, self.before)
1226 self.assertGreater(self.after, valid_since)
1227 return True
1228
1229 if key == 'last_auth':
1230 self.assertEqual(value, self.old_last_auth)
1231 return True
1232
1233 if key == 'account':
1234 expected = {
1235 'openid': self.account.openid_identifier,
1236 'email': self.mail.email,
1237 'displayname': "New test display name",
1238 'is_verified': self.account.is_verified,
1239 'username': self.account.person_name
1240 }
1241 actual = json.loads(base64.b64decode(value).decode("utf8"))
1242 self.assertEqual(expected, actual)
1243 return True
1244
1245 if key == 'expires':
1246 self.assertEqual(value, self.old_expires)
1247 return True
1248
1249 # we're not validating an SSO from the discharged macaroon, fail!
1250 return False
1251
1252 def get_value(self, discharge, service_location, search_key):
1253 for caveat in discharge.first_party_caveats():
1254 source, key, value = caveat.caveat_id.split("|", 2)
1255 if source == service_location and key == search_key:
1256 return value
1257
1212 def test_discharge_macaroon_corrupt(self):1258 def test_discharge_macaroon_corrupt(self):
1213 self.assertRaises(1259 self.assertRaises(
1214 ValidationError, refresh_discharge, "Seriously corrupted macaroon")1260 ValidationError, refresh_discharge, "Seriously corrupted macaroon")
@@ -1239,61 +1285,57 @@
1239 old_discharge = self.discharge_macaroon # just rename for readability1285 old_discharge = self.discharge_macaroon # just rename for readability
1240 service_location = settings.MACAROON_SERVICE_LOCATION1286 service_location = settings.MACAROON_SERVICE_LOCATION
12411287
1242 def get_value(search_key):
1243 for caveat in old_discharge.first_party_caveats():
1244 source, key, value = caveat.caveat_id.split("|", 2)
1245 if source == service_location and key == search_key:
1246 return value
1247
1248 # get old values from the macaroon and also change the account to see1288 # get old values from the macaroon and also change the account to see
1249 # that reflected1289 # that reflected
1250 old_last_auth = get_value('last_auth')1290 self.old_last_auth = self.get_value(
1251 old_expires = get_value('expires')1291 old_discharge, service_location, 'last_auth')
1252 new_mail = self.factory.make_email_for_account(1292 # expires isn't set by default, but we can test that it has no value
1293 self.old_expires = 'NOTSET'
1294 self.mail = self.factory.make_email_for_account(
1253 self.account, status=EmailStatus.PREFERRED)1295 self.account, status=EmailStatus.PREFERRED)
1254 self.account.displayname = "New test display name"1296 self.account.displayname = "New test display name"
1255 self.account.save()1297 self.account.save()
12561298
1257 # call!1299 # call!
1258 before = now()1300 self.before = now()
1259 new_discharge = refresh_discharge(old_discharge.serialize())1301 new_discharge = refresh_discharge(old_discharge.serialize())
1260 after = now()1302 self.after = now()
12611303
1262 # test1304 # verify using the NEW discharge macaroon
1263 def checker(caveat):1305 v = Verifier()
1264 """Assure all caveats inside the discharged macaroon are ok."""1306 v.satisfy_general(self.checker)
1265 source, key, value = caveat.split("|", 2)1307 v.verify(new_discharge, self.random_key, [])
12661308
1267 if key == 'valid_since':1309 def test_refreshing_with_expiry(self):
1268 valid_since = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')1310 expires = datetime.now() + timedelta(seconds=1000)
1269 self.assertGreater(valid_since, before)1311 caveat_info = _decrypt_caveat(self.caveat.caveat_id)
1270 self.assertGreater(after, valid_since)1312 # use _build_discharge as it can take an expiry
1271 return True1313 self.discharge_macaroon = _build_discharge(
12721314 caveat_info['3rdparty'],
1273 if key == 'last_auth':1315 self.caveat.caveat_id,
1274 self.assertEqual(value, old_last_auth)1316 self.account,
1275 return True1317 expires=expires.strftime(MACAROON_TIMESTAMP_FORMAT))
12761318
1277 if key == 'account':1319 service_location = settings.MACAROON_SERVICE_LOCATION
1278 expected = {1320
1279 'openid': self.account.openid_identifier,1321 # get old values from the macaroon and also change the account to see
1280 'email': new_mail.email,1322 # that reflected
1281 'displayname': "New test display name",1323 self.old_last_auth = self.get_value(
1282 'is_verified': self.account.is_verified,1324 self.discharge_macaroon, service_location, 'last_auth')
1283 'username': self.account.person_name1325 # grab our old expiry value
1284 }1326 self.old_expires = self.get_value(
1285 actual = json.loads(base64.b64decode(value).decode("utf8"))1327 self.discharge_macaroon, service_location, 'expires')
1286 self.assertEqual(expected, actual)1328 self.mail = self.factory.make_email_for_account(
1287 return True1329 self.account, status=EmailStatus.PREFERRED)
12881330 self.account.displayname = "New test display name"
1289 if key == 'expires':1331 self.account.save()
1290 self.assertEqual(value, old_expires)1332
1291 return True1333 # call!
12921334 self.before = now()
1293 # we're not validating an SSO from the discharged macaroon, fail!1335 new_discharge = refresh_discharge(self.discharge_macaroon.serialize())
1294 return False1336 self.after = now()
12951337
1296 # verify using the NEW discharge macaroon1338 # verify using the NEW discharge macaroon
1297 v = Verifier()1339 v = Verifier()
1298 v.satisfy_general(checker)1340 v.satisfy_general(self.checker)
1299 v.verify(new_discharge, self.random_key, [])1341 v.verify(new_discharge, self.random_key, [])