def test_get_process_auth_db_exceptions(self): """Ensure get_process_auth_db() handles DB exceptions well.""" # Prepare several instances of AuthDB to be used in mocks. auth_db_v0 = api.AuthDB(entity_group_version=0) auth_db_v1 = api.AuthDB(entity_group_version=1) # Fetch initial copy of AuthDB. self.set_time(0) self.set_fetched_auth_db(auth_db_v0) self.assertEqual(auth_db_v0, api.get_process_auth_db()) # Make process cache expire. self.set_time(api.get_process_cache_expiration_sec() + 1) # Emulate an exception in fetch_auth_db. def mock_fetch_auth_db(*_kwargs): raise Exception('Boom!') self.mock(api, 'fetch_auth_db', mock_fetch_auth_db) # Capture calls to logging.exception. logger_calls = [] self.mock(api.logging, 'exception', lambda *_args: logger_calls.append(1)) # Should return older copy of auth_db_v0 and log the exception. self.assertEqual(auth_db_v0, api.get_process_auth_db()) self.assertEqual(1, len(logger_calls)) # Make fetch_auth_db to work again. Verify get_process_auth_db() works too. self.set_fetched_auth_db(auth_db_v1) self.assertEqual(auth_db_v1, api.get_process_auth_db())
def test_allowed_clock_drift(self): now = utils.utcnow() self.mock_now(now) tok = fake_subtoken_proto('user:[email protected]') # Works -29 sec before activation. self.mock_now(now, -29) self.assertTrue( delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())) # Doesn't work before that. self.mock_now(now, -31) with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())
def test_expiration_moment(self): now = utils.utcnow() self.mock_now(now) tok = fake_subtoken_proto('user:[email protected]', validity_duration=3600) # Active at now + 3599. self.mock_now(now, 3599) self.assertTrue( delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())) # Expired at now + 3601. self.mock_now(now, 3601) with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())
def test_subtoken_services(self): tok = fake_subtoken_proto('user:[email protected]', services=['service:app-id']) # Passes. self.mock(model, 'get_service_self_identity', lambda: model.Identity.from_bytes('service:app-id')) self.assertTrue( delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())) # Fails. self.mock(model, 'get_service_self_identity', lambda: model.Identity.from_bytes('service:another-app-id')) with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())
def test_get_process_auth_db_multithreading(self): """Ensure get_process_auth_db() plays nice with multiple threads.""" def run_in_thread(func): """Runs |func| in a parallel thread, returns future (as Queue).""" result = Queue.Queue() thread = threading.Thread(target=lambda: result.put(func())) thread.start() return result # Prepare several instances of AuthDB to be used in mocks. auth_db_v0 = api.AuthDB(entity_group_version=0) auth_db_v1 = api.AuthDB(entity_group_version=1) # Run initial fetch, should cache |auth_db_v0| in process cache. self.set_time(0) self.set_fetched_auth_db(auth_db_v0) self.assertEqual(auth_db_v0, api.get_process_auth_db()) # Make process cache expire. self.set_time(api.get_process_cache_expiration_sec() + 1) # Start fetching AuthDB from another thread, at some point it will call # 'fetch_auth_db', and we pause the thread then and resume main thread. fetching_now = threading.Event() auth_db_queue = Queue.Queue() def mock_fetch_auth_db(**_kwargs): fetching_now.set() return auth_db_queue.get() self.mock(api, 'fetch_auth_db', mock_fetch_auth_db) future = run_in_thread(api.get_process_auth_db) # Wait for internal thread to call |fetch_auth_db|. fetching_now.wait() # Ok, now main thread is unblocked, while internal thread is blocking on a # artificially slow 'fetch_auth_db' call. Main thread can now try to get # AuthDB via get_process_auth_db(). It should get older stale copy right # away. self.assertEqual(auth_db_v0, api.get_process_auth_db()) # Finish background 'fetch_auth_db' call by returning 'auth_db_v1'. # That's what internal thread should get as result of 'get_process_auth_db'. auth_db_queue.put(auth_db_v1) self.assertEqual(auth_db_v1, future.get()) # Now main thread should get it as well. self.assertEqual(auth_db_v1, api.get_process_auth_db())
def test_get_group(self): g = model.AuthGroup( key=model.group_key('group'), members=[ model.Identity.from_bytes('user:[email protected]'), model.Identity.from_bytes('user:[email protected]'), ], globs=[model.IdentityGlob.from_bytes('user:*')], nested=['blah'], created_by=model.Identity.from_bytes('user:[email protected]'), created_ts=datetime.datetime(2014, 1, 2, 3, 4, 5), modified_by=model.Identity.from_bytes('user:[email protected]'), modified_ts=datetime.datetime(2015, 1, 2, 3, 4, 5)) db = api.AuthDB(groups=[g]) # Unknown group. self.assertIsNone(db.get_group('blah')) # Known group. from_cache = db.get_group('group') self.assertEqual(from_cache.key, g.key) # Members list is sorted. self.assertEqual(from_cache.members, [ model.Identity.from_bytes('user:[email protected]'), model.Identity.from_bytes('user:[email protected]'), ]) # Fields that are know to be different. exclude = ['members', 'auth_db_rev', 'auth_db_prev_rev'] self.assertEqual(from_cache.to_dict(exclude=exclude), g.to_dict(exclude=exclude))
def test_expired(self): now = int(utils.time_time()) tok = fake_subtoken_proto('user:[email protected]', creation_time=now - 120, validity_duration=60) with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())
def test_get_secret(self): # Make AuthDB with two secrets. secret = model.AuthSecret.bootstrap('some_secret') auth_db = api.AuthDB(secrets=[secret]) # Ensure they are accessible via get_secret. self.assertEqual(secret.values, auth_db.get_secret(api.SecretKey('some_secret')))
def test_get_process_auth_db_known_version(self): """Ensure get_process_auth_db() respects entity group version.""" # Prepare several instances of AuthDB to be used in mocks. auth_db_v0 = api.AuthDB(entity_group_version=0) auth_db_v0_again = api.AuthDB(entity_group_version=0) # Fetch initial copy of AuthDB. self.set_time(0) self.set_fetched_auth_db(auth_db_v0) self.assertEqual(auth_db_v0, api.get_process_auth_db()) # Make cache expire, but setup fetch_auth_db to return a new instance of # AuthDB, but with same entity group version. Old known instance of AuthDB # should be reused. self.set_time(api.get_process_cache_expiration_sec() + 1) self.set_fetched_auth_db(auth_db_v0_again) self.assertTrue(api.get_process_auth_db() is auth_db_v0)
def test_is_allowed_oauth_client_id(self): global_config = model.AuthGlobalConfig( oauth_client_id='1', oauth_additional_client_ids=['2', '3']) auth_db = api.AuthDB(global_config=global_config) self.assertFalse(auth_db.is_allowed_oauth_client_id(None)) self.assertTrue(auth_db.is_allowed_oauth_client_id('1')) self.assertTrue(auth_db.is_allowed_oauth_client_id('2')) self.assertTrue(auth_db.is_allowed_oauth_client_id('3')) self.assertFalse(auth_db.is_allowed_oauth_client_id('4'))
def test_get_latest_auth_db(self): """Ensure get_latest_auth_db "rushes" cached AuthDB update.""" auth_db_v0 = api.AuthDB(replication_state=mock_replication_state(0)) auth_db_v1 = api.AuthDB(replication_state=mock_replication_state(1)) # Fetch initial copy of AuthDB. self.set_time(0) self.set_fetched_auth_db(auth_db_v0) self.assertEqual(auth_db_v0, api.get_process_auth_db()) # Rig up fetch_auth_db to return a newer version. self.set_fetched_auth_db(auth_db_v1) # 'get_process_auth_db' still returns the cached one. self.assertEqual(auth_db_v0, api.get_process_auth_db()) # But 'get_latest_auth_db' returns a new one and updates the cached copy. self.assertEqual(auth_db_v1, api.get_latest_auth_db()) self.assertEqual(auth_db_v1, api.get_process_auth_db())
def test_get_process_auth_db_expiration(self): """Ensure get_process_auth_db() respects expiration.""" # Prepare several instances of AuthDB to be used in mocks. auth_db_v0 = api.AuthDB(entity_group_version=0) auth_db_v1 = api.AuthDB(entity_group_version=1) # Fetch initial copy of AuthDB. self.set_time(0) self.set_fetched_auth_db(auth_db_v0) self.assertEqual(auth_db_v0, api.get_process_auth_db()) # It doesn't expire for some time. self.set_time(api.get_process_cache_expiration_sec() - 1) self.set_fetched_auth_db(auth_db_v1) self.assertEqual(auth_db_v0, api.get_process_auth_db()) # But eventually it does. self.set_time(api.get_process_cache_expiration_sec() + 1) self.set_fetched_auth_db(auth_db_v1) self.assertEqual(auth_db_v1, api.get_process_auth_db())
def test_get_secret(self): # Make AuthDB with two secrets. local_secret = model.AuthSecret.bootstrap('local_secret', 'local') global_secret = model.AuthSecret.bootstrap('global_secret', 'global') auth_db = api.AuthDB(secrets=[local_secret, global_secret]) # Ensure they are accessible via get_secret. self.assertEqual( local_secret.values, auth_db.get_secret(api.SecretKey('local_secret', 'local'))) self.assertEqual( global_secret.values, auth_db.get_secret(api.SecretKey('global_secret', 'global')))
def test_is_allowed_oauth_client_id(self): global_config = model.AuthGlobalConfig( oauth_client_id='1', oauth_additional_client_ids=['2', '3']) auth_db = api.AuthDB(global_config=global_config, additional_client_ids=['local']) self.assertFalse(auth_db.is_allowed_oauth_client_id(None)) self.assertTrue(auth_db.is_allowed_oauth_client_id('1')) self.assertTrue(auth_db.is_allowed_oauth_client_id('2')) self.assertTrue(auth_db.is_allowed_oauth_client_id('3')) self.assertTrue(auth_db.is_allowed_oauth_client_id('local')) self.assertTrue( auth_db.is_allowed_oauth_client_id(api.API_EXPLORER_CLIENT_ID)) self.assertFalse(auth_db.is_allowed_oauth_client_id('4'))
def test_verify_ip_whitelisted_missing_whitelist(self): auth_db = api.AuthDB( ip_whitelist_assignments=model.AuthIPWhitelistAssignments( assignments=[ model.AuthIPWhitelistAssignments.Assignment( identity=model.Identity(model.IDENTITY_USER, '*****@*****.**'), ip_whitelist='missing ip whitelist', ) ], ), ) with self.assertRaises(api.AuthorizationError): auth_db.verify_ip_whitelisted( model.Identity(model.IDENTITY_USER, '*****@*****.**'), ipaddr.ip_from_string('127.0.0.1'), {})
def test_nested_groups_cycle(self): # Groups that nest each other. group1 = model.AuthGroup(id='Group1') group1.nested.append('Group2') group2 = model.AuthGroup(id='Group2') group2.nested.append('Group1') # Collect error messages. errors = [] self.mock(api.logging, 'error', lambda *args: errors.append(args)) # This should not hang, but produce error message. auth_db = api.AuthDB(groups=[group1, group2]) self.assertFalse(auth_db.is_group_member('Group1', model.Anonymous)) self.assertEqual(1, len(errors))
def test_get_secret_bootstrap(self): # Mock AuthSecret.bootstrap to capture calls to it. original = api.model.AuthSecret.bootstrap calls = [] @classmethod def mocked_bootstrap(cls, name, scope): calls.append((name, scope)) result = original(name, scope) result.values = ['123'] return result self.mock(api.model.AuthSecret, 'bootstrap', mocked_bootstrap) auth_db = api.AuthDB() got = auth_db.get_secret(api.SecretKey('local_secret', 'local')) self.assertEqual(['123'], got) self.assertEqual([('local_secret', 'local')], calls)
def test_nested_groups_cycle(self): # Groups that nest each other. group1 = model.AuthGroup(id='Group1') group1.nested.append('Group2') group2 = model.AuthGroup(id='Group2') group2.nested.append('Group1') # Collect warnings. warnings = [] self.mock(api.logging, 'warning', lambda msg, *_args: warnings.append(msg)) # This should not hang, but produce error message. auth_db = api.AuthDB(groups=[group1, group2]) self.assertFalse(auth_db.is_group_member('Group1', model.Anonymous)) self.assertEqual(1, len(warnings)) self.assertTrue('Cycle in a group graph' in warnings[0])
def test_subtoken_audience(self): auth_db = api.AuthDB(groups=[ model.AuthGroup( id='abc', members=[model.Identity.from_bytes('user:[email protected]')], ) ]) tok = fake_subtoken_proto('user:[email protected]', audience=['user:[email protected]', 'group:abc']) # Works. make_id = model.Identity.from_bytes self.assertTrue( delegation.check_subtoken(tok, make_id('user:[email protected]'), auth_db)) self.assertTrue( delegation.check_subtoken(tok, make_id('user:[email protected]'), auth_db)) # Other ids are rejected. with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, make_id('user:[email protected]'), auth_db)
def test_not_real_nested_group_cycle_aka_issue_251(self): # See https://github.com/luci/luci-py/issues/251. # # B -> A, C -> [B, A]. When traversing C, A is seen twice, and this is fine. group_A = model.AuthGroup(id='A') group_B = model.AuthGroup(id='B') group_C = model.AuthGroup(id='C') group_B.nested = ['A'] group_C.nested = ['A', 'B'] db = api.AuthDB(groups=[group_A, group_B, group_C]) # 'is_group_member' must not report 'Cycle in a group graph' warning. warnings = [] self.mock(api.logging, 'warning', lambda msg, *_args: warnings.append(msg)) self.assertFalse(db.is_group_member('C', model.Anonymous)) self.assertFalse(warnings)
def test_is_group_member(self): # Test identity. joe = model.Identity(model.IDENTITY_USER, '*****@*****.**') # Group that includes joe via glob. with_glob = model.AuthGroup(id='WithGlob') with_glob.globs.append( model.IdentityGlob(model.IDENTITY_USER, '*@example.com')) # Group that includes joe via explicit listing. with_listing = model.AuthGroup(id='WithListing') with_listing.members.append(joe) # Group that includes joe via nested group. with_nesting = model.AuthGroup(id='WithNesting') with_nesting.nested.append('WithListing') # Creates AuthDB with given list of groups and then runs the check. is_member = (lambda groups, identity, group: api.AuthDB(groups=groups). is_group_member(group, identity)) # Wildcard group includes everyone (even anonymous). self.assertTrue(is_member([], joe, '*')) self.assertTrue(is_member([], model.Anonymous, '*')) # An unknown group includes nobody. self.assertFalse(is_member([], joe, 'Missing')) self.assertFalse(is_member([], model.Anonymous, 'Missing')) # Globs are respected. self.assertTrue(is_member([with_glob], joe, 'WithGlob')) self.assertFalse(is_member([with_glob], model.Anonymous, 'WithGlob')) # Members lists are respected. self.assertTrue(is_member([with_listing], joe, 'WithListing')) self.assertFalse( is_member([with_listing], model.Anonymous, 'WithListing')) # Nested groups are respected. self.assertTrue( is_member([with_nesting, with_listing], joe, 'WithNesting')) self.assertFalse( is_member([with_nesting, with_listing], model.Anonymous, 'WithNesting'))
def make_auth_db_with_ip_whitelist(): """AuthDB with [email protected] assigned IP whitelist '127.0.0.1/32'.""" return api.AuthDB( ip_whitelists=[ model.AuthIPWhitelist( key=model.ip_whitelist_key('some ip whitelist'), subnets=['127.0.0.1/32'], ), model.AuthIPWhitelist( key=model.ip_whitelist_key('bots'), subnets=['192.168.1.1/32', '::1/32'], ), ], ip_whitelist_assignments=model.AuthIPWhitelistAssignments( assignments=[ model.AuthIPWhitelistAssignments.Assignment( identity=model.Identity(model.IDENTITY_USER, '*****@*****.**'), ip_whitelist='some ip whitelist',) ], ), )
def test_list_group(self): list_group = (lambda groups, group, recursive: api.AuthDB( groups=groups).list_group(group, recursive)) grp_1 = model.AuthGroup(id='1') grp_1.members.extend([ model.Identity(model.IDENTITY_USER, '*****@*****.**'), model.Identity(model.IDENTITY_USER, '*****@*****.**'), ]) grp_2 = model.AuthGroup(id='2') grp_2.nested.append('1') grp_2.members.extend([ # Specify 'b' again, even though it's in a nested group. model.Identity(model.IDENTITY_USER, '*****@*****.**'), model.Identity(model.IDENTITY_USER, '*****@*****.**'), ]) # Unknown group. self.assertEqual(set(), list_group([grp_1, grp_2], 'blah', False)) self.assertEqual(set(), list_group([grp_1, grp_2], 'blah', True)) # Non recursive. expected = set([ model.Identity(model.IDENTITY_USER, '*****@*****.**'), model.Identity(model.IDENTITY_USER, '*****@*****.**'), ]) self.assertEqual(expected, list_group([grp_1, grp_2], '2', False)) # Recursive. expected = set([ model.Identity(model.IDENTITY_USER, '*****@*****.**'), model.Identity(model.IDENTITY_USER, '*****@*****.**'), model.Identity(model.IDENTITY_USER, '*****@*****.**'), ]) self.assertEqual(expected, list_group([grp_1, grp_2], '2', True))
def list_group(groups, group, recursive): l = api.AuthDB(groups=groups).list_group(group, recursive) return api.GroupListing(sorted(l.members), sorted(l.globs), sorted(l.nested))
def build(self): return api.AuthDB(groups=self.groups)
def test_passes_validation(self): tok = fake_subtoken_proto('user:[email protected]') ident = delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB()) self.assertEqual('user:[email protected]', ident.to_bytes())
def test_negative_validatity_duration(self): tok = fake_subtoken_proto('user:[email protected]', validity_duration=-3600) with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())
def test_not_active_yet(self): now = int(utils.time_time()) tok = fake_subtoken_proto('user:[email protected]', creation_time=now + 120) with self.assertRaises(delegation.BadTokenError): delegation.check_subtoken(tok, FAKE_IDENT, api.AuthDB())
def test_get_secret_bad_scope(self): with self.assertRaises(ValueError): api.AuthDB().get_secret(api.SecretKey('some', 'bad-scope'))