44from beacon_api .conf .config import init_db_pool
55from beacon_api .api .query import access_resolution
66from beacon_api .utils .validate import token_scheme_check , verify_aud_claim
7- from beacon_api .permissions .ga4gh import get_ga4gh_controlled , get_ga4gh_bona_fide
7+ from beacon_api .permissions .ga4gh import get_ga4gh_controlled , get_ga4gh_bona_fide , validate_passport
8+ from beacon_api .permissions .ga4gh import check_ga4gh_token , decode_passport , get_ga4gh_permissions
9+ from deploy .test .mock_auth import generate_token
810from .test_app import PARAMS
911from testfixtures import TempDirectory
1012from test .support import EnvironmentVarGuard
13+ # Hack to import a function from beyond top-level package
14+ # used for test_decode_passport() to get a real token
15+ import sys
16+ sys .path .append (".." )
1117
1218
1319def mock_token (bona_fide , permissions , auth ):
@@ -17,6 +23,21 @@ def mock_token(bona_fide, permissions, auth):
1723 "authenticated" : auth }
1824
1925
26+ class MockDecodedPassport :
27+ """Mock JWT."""
28+
29+ def __init__ (self , validated = True ):
30+ """Initialise mock JWT."""
31+ self .validated = validated
32+
33+ def validate (self ):
34+ """Invoke validate."""
35+ if self .validated :
36+ return True
37+ else :
38+ raise Exception
39+
40+
2041class MockBeaconDB :
2142 """BeaconDB mock.
2243
@@ -290,31 +311,37 @@ def test_access_resolution_controlled_never_reached2(self):
290311 with self .assertRaises (aiohttp .web_exceptions .HTTPForbidden ):
291312 access_resolution (request , token , host , [], [], [8 ])
292313
293- async def test_ga4gh_controlled (self ):
314+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.validate_passport' )
315+ async def test_ga4gh_controlled (self , m_validation ):
294316 """Test ga4gh permissions claim parsing."""
295- # Good test: claims OK, userinfo OK
296- token_claim = []
297- datasets = await get_ga4gh_controlled (token_claim )
298- self .assertEqual (datasets , set ()) # has permissions
299- # # Bad test: no claims, userinfo OK
300- # token_claim = []
301- # token = 'this_is_a_jwt'
302- # datasets = await get_ga4gh_controlled(token, token_claim)
303- # self.assertEqual(datasets, set()) # doesn't have permissions
304- # # Bad test: claims OK, no userinfo
305- # userinfo.return_value = {}
306- # token_claim = ["ga4gh.ControlledAccessGrants"]
307- # token = 'this_is_a_jwt'
308- # datasets = await get_ga4gh_controlled(token, token_claim)
309- # self.assertEqual(datasets, set()) # doesn't have permissions
310- # # Bad test: no claims, no userinfo
311- # token_claim = []
312- # token = 'this_is_a_jwt'
313- # datasets = await get_ga4gh_controlled(token, token_claim)
314- # self.assertEqual(datasets, set()) # doesn't have permissions
315-
316- @asynctest .mock .patch ('beacon_api.permissions.ga4gh.retrieve_user_data' )
317- async def test_ga4gh_bona_fide (self , userinfo ):
317+ # Test: no passports, no permissions
318+ datasets = await get_ga4gh_controlled ([])
319+ self .assertEqual (datasets , set ())
320+ # Test: 1 passport, 1 unique dataset, 1 permission
321+ passport = {"ga4gh_visa_v1" : {"type" : "ControlledAccessGrants" ,
322+ "value" : "https://institution.org/EGAD01" ,
323+ "source" : "https://ga4gh.org/duri/no_org" ,
324+ "by" : "self" ,
325+ "asserted" : 1539069213 ,
326+ "expires" : 4694742813 }}
327+ m_validation .return_value = passport
328+ dataset = await get_ga4gh_controlled ([{}]) # one passport
329+ self .assertEqual (dataset , {'EGAD01' })
330+ # Test: 2 passports, 1 unique dataset, 1 permission (permissions must not be duplicated)
331+ passport = {"ga4gh_visa_v1" : {"type" : "ControlledAccessGrants" ,
332+ "value" : "https://institution.org/EGAD01" ,
333+ "source" : "https://ga4gh.org/duri/no_org" ,
334+ "by" : "self" ,
335+ "asserted" : 1539069213 ,
336+ "expires" : 4694742813 }}
337+ m_validation .return_value = passport
338+ dataset = await get_ga4gh_controlled ([{}, {}]) # two passports
339+ self .assertEqual (dataset , {'EGAD01' })
340+ # Test: 2 passports, 2 unique datasets, 2 permissions
341+ # Can't test this case with the current design!
342+ # Would need a way for validate_passport() to mock two different results
343+
344+ async def test_ga4gh_bona_fide (self ):
318345 """Test ga4gh statuses claim parsing."""
319346 passports = [("enc" , "header" , {
320347 "ga4gh_visa_v1" : {"type" : "AcceptedTermsAndPolicies" ,
@@ -331,14 +358,102 @@ async def test_ga4gh_bona_fide(self, userinfo):
331358 "by" : "peer" ,
332359 "asserted" : 1539017776 ,
333360 "expires" : 1593165413 }})]
334- # Good test: claims OK, userinfo OK
361+ # Good test: both required passport types contained the correct value
335362 bona_fide_status = await get_ga4gh_bona_fide (passports )
336363 self .assertEqual (bona_fide_status , True ) # has bona fide
337- # Bad test: no claims, userinfo OK
364+ # Bad test: missing passports of required type
338365 passports_empty = []
339366 bona_fide_status = await get_ga4gh_bona_fide (passports_empty )
340367 self .assertEqual (bona_fide_status , False ) # doesn't have bona fide
341368
369+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.get_jwk' )
370+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.jwt' )
371+ async def test_validate_passport (self , m_jwt , m_jwk ):
372+ """Test passport validation."""
373+ m_jwk .return_value = 'jwk'
374+ # Test: validation passed
375+ m_jwt .return_value = MockDecodedPassport ()
376+ await validate_passport ({})
377+ #
378+ # This test doesn't work
379+ #
380+ # # Test: validation failed
381+ # m_jwt.return_value = MockDecodedPassport(validated=False)
382+ # with self.assertRaises(Exception):
383+ # await validate_passport({})
384+
385+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.get_ga4gh_permissions' )
386+ async def test_check_ga4gh_token (self , m_get_perms ):
387+ """Test token scopes."""
388+ # Test: no scope found
389+ decoded_data = {}
390+ dataset_permissions , bona_fide_status = await check_ga4gh_token (decoded_data , {}, False , set ())
391+ self .assertEqual (dataset_permissions , set ())
392+ self .assertEqual (bona_fide_status , False )
393+ # Test: scope is ok, but no claims
394+ decoded_data = {'scope' : '' }
395+ dataset_permissions , bona_fide_status = await check_ga4gh_token (decoded_data , {}, False , set ())
396+ self .assertEqual (dataset_permissions , set ())
397+ self .assertEqual (bona_fide_status , False )
398+ # Test: scope is ok, claims are ok
399+ m_get_perms .return_value = {'EGAD01' }, True
400+ decoded_data = {'scope' : 'openid ga4gh_passport_v1' }
401+ dataset_permissions , bona_fide_status = await check_ga4gh_token (decoded_data , {}, False , set ())
402+ self .assertEqual (dataset_permissions , {'EGAD01' })
403+ self .assertEqual (bona_fide_status , True )
404+
405+ async def test_decode_passport (self ):
406+ """Test key-less JWT decoding."""
407+ _ , token , _ = generate_token ()
408+ header , payload = await decode_passport (token )
409+ self .assertEqual (header .get ('alg' ), 'RS256' )
410+ self .assertEqual (payload .get ('iss' ), 'http://test.csc.fi' )
411+
412+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.get_ga4gh_bona_fide' )
413+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.get_ga4gh_controlled' )
414+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.decode_passport' )
415+ @asynctest .mock .patch ('beacon_api.permissions.ga4gh.retrieve_user_data' )
416+ async def test_get_ga4gh_permissions (self , m_userinfo , m_decode , m_controlled , m_bonafide ):
417+ """Test GA4GH permissions main function."""
418+ # Test: no data (nothing)
419+ m_userinfo .return_value = [{}]
420+ header = {}
421+ payload = {}
422+ m_decode .return_value = header , payload
423+ m_controlled .return_value = set ()
424+ m_bonafide .return_value = False
425+ dataset_permissions , bona_fide_status = await get_ga4gh_permissions ('token' )
426+ self .assertEqual (dataset_permissions , set ())
427+ self .assertEqual (bona_fide_status , False )
428+ # Test: permissions
429+ m_userinfo .return_value = [{}]
430+ header = {}
431+ payload = {
432+ 'ga4gh_visa_v1' : {
433+ 'type' : 'ControlledAccessGrants'
434+ }
435+ }
436+ m_decode .return_value = header , payload
437+ m_controlled .return_value = {'EGAD01' }
438+ m_bonafide .return_value = False
439+ dataset_permissions , bona_fide_status = await get_ga4gh_permissions ('token' )
440+ self .assertEqual (dataset_permissions , {'EGAD01' })
441+ self .assertEqual (bona_fide_status , False )
442+ # Test: bona fide
443+ m_userinfo .return_value = [{}]
444+ header = {}
445+ payload = {
446+ 'ga4gh_visa_v1' : {
447+ 'type' : 'ResearcherStatus'
448+ }
449+ }
450+ m_decode .return_value = header , payload
451+ m_controlled .return_value = set ()
452+ m_bonafide .return_value = True
453+ dataset_permissions , bona_fide_status = await get_ga4gh_permissions ('token' )
454+ self .assertEqual (dataset_permissions , set ())
455+ self .assertEqual (bona_fide_status , True )
456+
342457
343458if __name__ == '__main__' :
344459 asynctest .main ()
0 commit comments