Move sync team into its own module and add tests

This commit is contained in:
Joseph Schorr 2017-02-22 18:34:05 -05:00
parent b683088f87
commit 938730c076
4 changed files with 350 additions and 98 deletions

View file

@ -410,6 +410,15 @@ def list_team_users(team):
.where(Team.id == team, User.robot == False)) .where(Team.id == team, User.robot == False))
def list_team_robots(team):
""" Returns an iterator of all the *robots* found in a team. Does not include users. """
return (User
.select()
.join(TeamMember)
.join(Team)
.where(Team.id == team, User.robot == True))
def set_team_syncing(team, login_service_name, config): def set_team_syncing(team, login_service_name, config):
""" Sets the given team to sync to the given service using the given config. """ """ Sets the given team to sync to the given service using the given config. """
login_service = LoginService.get(name=login_service_name) login_service = LoginService.get(name=login_service_name)

119
data/users/teamsync.py Normal file
View file

@ -0,0 +1,119 @@
import logging
import json
from data import model
logger = logging.getLogger(__name__)
def sync_teams_to_groups(authentication, stale_cutoff):
""" Performs team syncing by looking up any stale team(s) found, and performing the sync
operation on them.
"""
logger.debug('Looking up teams to sync to groups')
sync_team_tried = set()
while True:
# Find a stale team.
stale_team_sync = model.team.get_stale_team(stale_cutoff)
if not stale_team_sync:
logger.debug('No additional stale team found; sleeping')
return
# Make sure we don't try to reprocess a team on this iteration.
if stale_team_sync.id in sync_team_tried:
break
sync_team_tried.add(stale_team_sync.id)
# Sync the team.
if not sync_team(authentication, stale_team_sync):
return
def sync_team(authentication, stale_team_sync):
""" Performs synchronization of a team (as referenced by the TeamSync stale_team_sync).
Returns True on success and False otherwise.
"""
sync_config = json.loads(stale_team_sync.config)
logger.info('Syncing team `%s` under organization %s via %s (#%s)', stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config, stale_team_sync.team_id)
# Load all the existing members of the team in Quay that are bound to the auth service.
existing_users = model.team.list_federated_team_members(stale_team_sync.team,
authentication.federated_service)
logger.debug('Existing membership of %s for team `%s` under organization %s via %s (#%s)',
len(existing_users), stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config, stale_team_sync.team_id)
# Load all the members of the team from the authenication system.
(member_iterator, err) = authentication.iterate_group_members(sync_config)
if err is not None:
logger.error('Got error when trying to iterate group members with config %s: %s',
sync_config, err)
return False
# Collect all the members currently found in the group, adding them to the team as we go
# along.
group_membership = set()
for (member_info, err) in member_iterator:
if err is not None:
logger.error('Got error when trying to construct a member: %s', err)
continue
# If the member is already in the team, nothing more to do.
if member_info.username in existing_users:
logger.debug('Member %s already in team `%s` under organization %s via %s (#%s)',
member_info.username, stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config,
stale_team_sync.team_id)
group_membership.add(existing_users[member_info.username])
continue
# Retrieve the Quay user associated with the member info.
(quay_user, err) = authentication.get_federated_user(member_info)
if err is not None:
logger.error('Could not link external user %s to an internal user: %s',
member_info.username, err)
continue
# Add the user to the membership set.
group_membership.add(quay_user.id)
# Add the user to the team.
try:
logger.info('Adding member %s to team `%s` under organization %s via %s (#%s)',
quay_user.username, stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config,
stale_team_sync.team_id)
model.team.add_user_to_team(quay_user, stale_team_sync.team)
except model.UserAlreadyInTeam:
# If the user is already present, nothing more to do for them.
pass
# Update the transaction and last_updated time of the team sync. Only if it matches
# the current value will we then perform the deletion step.
result = model.team.update_sync_status(stale_team_sync)
if not result:
# Another worker updated this team. Nothing more to do.
logger.debug('Another worker synced team `%s` under organization %s via %s (#%s)',
stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config,
stale_team_sync.team_id)
return True
# Delete any team members not found in the backing auth system.
logger.debug('Deleting stale members for team `%s` under organization %s via %s (#%s)',
stale_team_sync.team.name, stale_team_sync.team.organization.username,
sync_config, stale_team_sync.team_id)
deleted = model.team.delete_members_not_present(stale_team_sync.team, group_membership)
# Done!
logger.info('Finishing sync for team `%s` under organization %s via %s (#%s): %s deleted',
stale_team_sync.team.name, stale_team_sync.team.organization.username,
sync_config, stale_team_sync.team_id, deleted)
return True

View file

@ -0,0 +1,220 @@
import pytest
from datetime import timedelta
from mock import patch
from data import model, database
from data.users.federated import FederatedUsers, UserInformation
from data.users.teamsync import sync_team, sync_teams_to_groups
from endpoints.test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
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.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']),
])
def test_syncing(starting_membership, group_membership, expected_membership, 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.list_federated_team_members(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(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.
sync_teams_to_groups(fake_auth, timedelta(seconds=120))
third_sync_info = model.team.get_team_sync_information('buynlarge', 'synced')
assert third_sync_info.last_updated == updated_sync_info.last_updated
assert third_sync_info.transaction_id == updated_sync_info.transaction_id
# Set the stale threshold to -1 seconds, and ensure the team is resynced.
sync_teams_to_groups(fake_auth, timedelta(seconds=-1))
fourth_sync_info = model.team.get_team_sync_information('buynlarge', 'synced')
assert fourth_sync_info.transaction_id != updated_sync_info.transaction_id

View file

@ -1,9 +1,8 @@
import logging import logging
import time import time
import json
from app import app, authentication from app import app, authentication
from data import model from data.users.teamsync import sync_teams_to_groups
from workers.worker import Worker from workers.worker import Worker
from util.timedeltastring import convert_to_timedelta from util.timedeltastring import convert_to_timedelta
@ -20,103 +19,8 @@ class TeamSynchronizationWorker(Worker):
self.add_operation(self._sync_teams_to_groups, WORKER_FREQUENCY) self.add_operation(self._sync_teams_to_groups, WORKER_FREQUENCY)
def _sync_teams_to_groups(self): def _sync_teams_to_groups(self):
logger.debug('Looking up teams to sync to groups') sync_teams_to_groups(authentication, STALE_CUTOFF)
sync_team_tried = set()
while True:
# Find a stale team.
stale_team_sync = model.team.get_stale_team(STALE_CUTOFF)
if not stale_team_sync:
logger.debug('No additional stale team found; sleeping')
return
# Make sure we don't try to reprocess a team on this iteration.
if stale_team_sync.id in sync_team_tried:
continue
sync_team_tried.add(stale_team_sync.id)
sync_config = json.loads(stale_team_sync.config)
logger.info('Syncing team `%s` under organization %s via %s (#%s)', stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config, stale_team_sync.team_id)
# Load all the existing members of the team in Quay that are bound to the auth service.
existing_users = model.team.list_federated_team_members(stale_team_sync.team,
authentication.federated_service)
logger.debug('Existing membership of %s for team `%s` under organization %s via %s (#%s)',
len(existing_users), stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config, stale_team_sync.team_id)
# Load all the members of the team from the authenication system.
(member_iterator, err) = authentication.iterate_group_members(sync_config)
if err is not None:
logger.error('Got error when trying to iterate group members with config %s: %s',
sync_config, err)
continue
# Collect all the members currently found in the group, adding them to the team as we go
# along.
group_membership = set()
for (member_info, err) in member_iterator:
if err is not None:
logger.error('Got error when trying to construct a member: %s', err)
continue
# If the member is already in the team, nothing more to do.
if member_info.username in existing_users:
logger.debug('Member %s already in team `%s` under organization %s via %s (#%s)',
member_info.username, stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config,
stale_team_sync.team_id)
group_membership.add(existing_users[member_info.username])
continue
# Retrieve the Quay user associated with the member info.
(quay_user, err) = authentication.get_federated_user(member_info)
if err is not None:
logger.error('Could not link external user %s to an internal user: %s',
member_info.username, err)
continue
# Add the user to the membership set.
group_membership.add(quay_user.id)
# Add the user to the team.
try:
logger.info('Adding member %s to team `%s` under organization %s via %s (#%s)',
quay_user.username, stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config,
stale_team_sync.team_id)
model.team.add_user_to_team(quay_user, stale_team_sync.team)
except model.UserAlreadyInTeam:
# If the user is already present, nothing more to do for them.
pass
# Update the transaction and last_updated time of the team sync. Only if it matches
# the current value will we then perform the deletion step.
result = model.team.update_sync_status(stale_team_sync)
if not result:
# Another worker updated this team. Nothing more to do.
logger.debug('Another worker synced team `%s` under organization %s via %s (#%s)',
stale_team_sync.team.name,
stale_team_sync.team.organization.username, sync_config,
stale_team_sync.team_id)
continue
# Delete any team members not found in the backing auth system.
logger.debug('Deleting stale members for team `%s` under organization %s via %s (#%s)',
stale_team_sync.team.name, stale_team_sync.team.organization.username,
sync_config, stale_team_sync.team_id)
deleted = model.team.delete_members_not_present(stale_team_sync.team, group_membership)
# Done!
logger.info('Finishing sync for team `%s` under organization %s via %s (#%s): %s deleted',
stale_team_sync.team.name, stale_team_sync.team.organization.username,
sync_config, stale_team_sync.team_id, deleted)
def main(): def main():
logging.config.fileConfig('conf/logging_debug.conf', disable_existing_loggers=False) logging.config.fileConfig('conf/logging_debug.conf', disable_existing_loggers=False)