import os from datetime import datetime, timedelta import pytest from mock import patch from data import database, model from data.users.federated import FederatedUsers, UserInformation from data.users.teamsync import sync_team, sync_teams_to_groups from test.fixtures import * from test.test_keystone_auth import fake_keystone from test.test_ldap import mock_ldap from util.names import parse_robot_username _FAKE_AUTH = "fake" class FakeUsers(FederatedUsers): def __init__(self, group_members): super(FakeUsers, self).__init__(_FAKE_AUTH, False) self.group_tuples = [(m, None) for m in group_members] def iterate_group_members(self, group_lookup_args, page_size=None, disable_pagination=False): return (self.group_tuples, None) @pytest.fixture(params=[True, False]) def user_creation(request): with patch("features.USER_CREATION", request.param): yield @pytest.fixture(params=[True, False]) def invite_only_user_creation(request): with patch("features.INVITE_ONLY_USER_CREATION", request.param): yield @pytest.fixture(params=[True, False]) def blacklisted_emails(request): mock_blacklisted_domains = {"BLACKLISTED_EMAIL_DOMAINS": ["blacklisted.com", "blacklisted.net"]} with patch("features.BLACKLISTED_EMAILS", request.param): with patch.dict("data.model.config.app_config", mock_blacklisted_domains): yield @pytest.mark.skipif( os.environ.get("TEST_DATABASE_URI", "").find("postgres") >= 0, reason="Postgres fails when existing members are added under the savepoint", ) @pytest.mark.parametrize( "starting_membership,group_membership,expected_membership", [ # Empty team + single member in group => Single member in team. ( [], [ UserInformation("someuser", "someuser", "someuser@devtable.com"), ], ["someuser"], ), # Team with a Quay user + empty group => empty team. ([("someuser", None)], [], []), # Team with an existing external user + user is in the group => no changes. ( [ ("someuser", "someuser"), ], [ UserInformation("someuser", "someuser", "someuser@devtable.com"), ], ["someuser"], ), # Team with an existing external user (with a different Quay username) + user is in the group. # => no changes ( [ ("anotherquayname", "someuser"), ], [ UserInformation("someuser", "someuser", "someuser@devtable.com"), ], ["someuser"], ), # Team missing a few members that are in the group => members added. ( [("someuser", "someuser")], [ UserInformation("anotheruser", "anotheruser", "anotheruser@devtable.com"), UserInformation("someuser", "someuser", "someuser@devtable.com"), UserInformation("thirduser", "thirduser", "thirduser@devtable.com"), ], ["anotheruser", "someuser", "thirduser"], ), # Team has a few extra members no longer in the group => members removed. ( [ ("anotheruser", "anotheruser"), ("someuser", "someuser"), ("thirduser", "thirduser"), ("nontestuser", None), ], [ UserInformation("thirduser", "thirduser", "thirduser@devtable.com"), ], ["thirduser"], ), # Team has different membership than the group => members added and removed. ( [ ("anotheruser", "anotheruser"), ("someuser", "someuser"), ("nontestuser", None), ], [ UserInformation("anotheruser", "anotheruser", "anotheruser@devtable.com"), UserInformation("missinguser", "missinguser", "missinguser@devtable.com"), ], ["anotheruser", "missinguser"], ), # Team has same membership but some robots => robots remain and no other changes. ( [ ("someuser", "someuser"), ("buynlarge+anotherbot", None), ("buynlarge+somerobot", None), ], [ UserInformation("someuser", "someuser", "someuser@devtable.com"), ], ["someuser", "buynlarge+somerobot", "buynlarge+anotherbot"], ), # Team has an extra member and some robots => member removed and robots remain. ( [ ("someuser", "someuser"), ("buynlarge+anotherbot", None), ("buynlarge+somerobot", None), ], [ # No members. ], ["buynlarge+somerobot", "buynlarge+anotherbot"], ), # Team has a different member and some robots => member changed and robots remain. ( [ ("someuser", "someuser"), ("buynlarge+anotherbot", None), ("buynlarge+somerobot", None), ], [ UserInformation("anotheruser", "anotheruser", "anotheruser@devtable.com"), ], ["anotheruser", "buynlarge+somerobot", "buynlarge+anotherbot"], ), # Team with an existing external user (with a different Quay username) + user is in the group. # => no changes and robots remain. ( [ ("anotherquayname", "someuser"), ("buynlarge+anotherbot", None), ], [ UserInformation("someuser", "someuser", "someuser@devtable.com"), ], ["someuser", "buynlarge+anotherbot"], ), # Team which returns the same member twice, as pagination in some engines (like LDAP) is not # stable. ( [], [ UserInformation("someuser", "someuser", "someuser@devtable.com"), UserInformation("anotheruser", "anotheruser", "anotheruser@devtable.com"), UserInformation("someuser", "someuser", "someuser@devtable.com"), ], ["anotheruser", "someuser"], ), ], ) def test_syncing( user_creation, invite_only_user_creation, starting_membership, group_membership, expected_membership, blacklisted_emails, app, ): org = model.organization.get_organization("buynlarge") # Necessary for the fake auth entries to be created in FederatedLogin. database.LoginService.create(name=_FAKE_AUTH) # Assert the team is empty, so we have a clean slate. sync_team_info = model.team.get_team_sync_information("buynlarge", "synced") assert len(list(model.team.list_team_users(sync_team_info.team))) == 0 # Add the existing starting members to the team. for starting_member in starting_membership: (quay_username, fakeauth_username) = starting_member if "+" in quay_username: # Add a robot. (_, shortname) = parse_robot_username(quay_username) robot, _ = model.user.create_robot(shortname, org) model.team.add_user_to_team(robot, sync_team_info.team) else: email = quay_username + "@devtable.com" if fakeauth_username is None: quay_user = model.user.create_user_noverify(quay_username, email) else: quay_user = model.user.create_federated_user( quay_username, email, _FAKE_AUTH, fakeauth_username, False ) model.team.add_user_to_team(quay_user, sync_team_info.team) # Call syncing on the team. fake_auth = FakeUsers(group_membership) assert sync_team(fake_auth, sync_team_info) # Ensure the last updated time and transaction_id's have changed. updated_sync_info = model.team.get_team_sync_information("buynlarge", "synced") assert updated_sync_info.last_updated is not None assert updated_sync_info.transaction_id != sync_team_info.transaction_id users_expected = set([name for name in expected_membership if "+" not in name]) robots_expected = set([name for name in expected_membership if "+" in name]) assert len(users_expected) + len(robots_expected) == len(expected_membership) # Check that the team's users match those expected. service_user_map = model.team.get_federated_team_member_mapping(sync_team_info.team, _FAKE_AUTH) assert set(service_user_map.keys()) == users_expected quay_users = model.team.list_team_users(sync_team_info.team) assert len(quay_users) == len(users_expected) for quay_user in quay_users: fakeauth_record = model.user.lookup_federated_login(quay_user, _FAKE_AUTH) assert fakeauth_record is not None assert fakeauth_record.service_ident in users_expected assert service_user_map[fakeauth_record.service_ident] == quay_user.id # Check that the team's robots match those expected. robots_found = set([r.username for r in model.team.list_team_robots(sync_team_info.team)]) assert robots_expected == robots_found def test_sync_teams_to_groups(user_creation, invite_only_user_creation, blacklisted_emails, app): # Necessary for the fake auth entries to be created in FederatedLogin. database.LoginService.create(name=_FAKE_AUTH) # Assert the team has not yet been updated. sync_team_info = model.team.get_team_sync_information("buynlarge", "synced") assert sync_team_info.last_updated is None # Call to sync all teams. fake_auth = FakeUsers([]) sync_teams_to_groups(fake_auth, timedelta(seconds=1)) # Ensure the team was synced. updated_sync_info = model.team.get_team_sync_information("buynlarge", "synced") assert updated_sync_info.last_updated is not None assert updated_sync_info.transaction_id != sync_team_info.transaction_id # Set the stale threshold to a high amount and ensure the team is not resynced. current_info = model.team.get_team_sync_information("buynlarge", "synced") current_info.last_updated = datetime.now() - timedelta(seconds=2) current_info.save() sync_teams_to_groups(fake_auth, timedelta(seconds=120)) third_sync_info = model.team.get_team_sync_information("buynlarge", "synced") assert third_sync_info.transaction_id == updated_sync_info.transaction_id # Set the stale threshold to 10 seconds, and ensure the team is resynced, after making it # "updated" 20s ago. current_info = model.team.get_team_sync_information("buynlarge", "synced") current_info.last_updated = datetime.now() - timedelta(seconds=20) current_info.save() sync_teams_to_groups(fake_auth, timedelta(seconds=10)) fourth_sync_info = model.team.get_team_sync_information("buynlarge", "synced") assert fourth_sync_info.transaction_id != updated_sync_info.transaction_id @pytest.mark.parametrize( "auth_system_builder,config", [ (mock_ldap, {"group_dn": "cn=AwesomeFolk"}), (fake_keystone, {"group_id": "somegroupid"}), ], ) def test_teamsync_end_to_end( user_creation, invite_only_user_creation, auth_system_builder, config, blacklisted_emails, app ): with auth_system_builder() as auth: # Create an new team to sync. org = model.organization.get_organization("buynlarge") new_synced_team = model.team.create_team("synced2", org, "member", "Some synced team.") sync_team_info = model.team.set_team_syncing( new_synced_team, auth.federated_service, config ) # Sync the team. assert sync_team(auth, sync_team_info) # Ensure we now have members. msg = "Auth system: %s" % auth.federated_service sync_team_info = model.team.get_team_sync_information("buynlarge", "synced2") team_members = list(model.team.list_team_users(sync_team_info.team)) number_of_members = len(team_members) assert len(team_members) > 1, msg it, _ = auth.iterate_group_members(config) assert len(team_members) == len(list(it)), msg sync_team_info.last_updated = datetime.now() - timedelta(hours=6) sync_team_info.save() # Remove one of the members and force a sync again to ensure we re-link the correct users. first_member = team_members[0] model.team.remove_user_from_team("buynlarge", "synced2", first_member.username, "devtable") team_members2 = list(model.team.list_team_users(sync_team_info.team)) assert len(team_members2) == number_of_members - 1, msg assert sync_team(auth, sync_team_info) team_members3 = list(model.team.list_team_users(sync_team_info.team)) assert len(team_members3) > 1, msg assert set([m.id for m in team_members]) == set([m.id for m in team_members3]) @pytest.mark.parametrize( "auth_system_builder,config", [ (mock_ldap, {"group_dn": "cn=AwesomeFolk"}), (fake_keystone, {"group_id": "somegroupid"}), ], ) def test_teamsync_existing_email( user_creation, invite_only_user_creation, auth_system_builder, blacklisted_emails, config, app ): with auth_system_builder() as auth: # Create an new team to sync. org = model.organization.get_organization("buynlarge") new_synced_team = model.team.create_team("synced2", org, "member", "Some synced team.") sync_team_info = model.team.set_team_syncing( new_synced_team, auth.federated_service, config ) # Add a new *unlinked* user with the same email address as one of the team members. it, _ = auth.iterate_group_members(config) members = list(it) model.user.create_user_noverify("someusername", members[0][0].email) # Sync the team and ensure it doesn't fail. assert sync_team(auth, sync_team_info) team_members = list(model.team.list_team_users(sync_team_info.team)) assert len(team_members) > 0 def test_get_stale_team_for_unsupported_login_service(initialized_db): dev_user = model.user.get_user("devtable") org = model.organization.create_organization("testorg", "testorg" + "@example.com", dev_user) database.LoginService.create(name=_FAKE_AUTH) team1 = model.team.create_team("team1", org, "member", "Some synced team.") model.team.set_team_syncing(team1, "oidc", {"group_dn": "cn=Test-Group,ou=Users"}) sync_team_info = model.team.get_team_sync_information("testorg", "team1") assert sync_team_info.last_updated is None fake_auth = FakeUsers([]) sync_teams_to_groups(fake_auth, timedelta(seconds=20)) updated_sync_info = model.team.get_team_sync_information("testorg", "team1") assert updated_sync_info.last_updated is None