diff --git a/data/database.py b/data/database.py index 1914a954c..6562e1f63 100644 --- a/data/database.py +++ b/data/database.py @@ -90,6 +90,15 @@ def close_db_filter(_): read_slave.close() +class QuayUserField(ForeignKeyField): + def __init__(self, allows_robots=False, *args, **kwargs): + self.allows_robots = allows_robots + if not 'rel_model' in kwargs: + kwargs['rel_model'] = User + + super(QuayUserField, self).__init__(*args, **kwargs) + + class BaseModel(ReadSlaveModel): class Meta: database = db @@ -109,6 +118,19 @@ class User(BaseModel): invalid_login_attempts = IntegerField(default=0) last_invalid_login = DateTimeField(default=datetime.utcnow) + def delete_instance(self, recursive=False, delete_nullable=False): + # If we are deleting a robot account, only execute the subset of queries necessary. + if self.robot: + # For all the model dependencies, only delete those that allow robots. + for query, fk in self.dependencies(search_nullable=True): + if isinstance(fk, QuayUserField) and fk.allows_robots: + model = fk.model_class + model.delete().where(query).execute() + + # Delete the instance itself. + super(User, self).delete_instance(recursive=False, delete_nullable=False) + else: + super(User, self).delete_instance(recursive=recursive, delete_nullable=delete_nullable) class TeamRole(BaseModel): name = CharField(index=True) @@ -116,7 +138,7 @@ class TeamRole(BaseModel): class Team(BaseModel): name = CharField(index=True) - organization = ForeignKeyField(User, index=True) + organization = QuayUserField(index=True) role = ForeignKeyField(TeamRole) description = TextField(default='') @@ -130,7 +152,7 @@ class Team(BaseModel): class TeamMember(BaseModel): - user = ForeignKeyField(User, index=True) + user = QuayUserField(allows_robots=True, index=True) team = ForeignKeyField(Team, index=True) class Meta: @@ -144,7 +166,7 @@ class TeamMember(BaseModel): class TeamMemberInvite(BaseModel): # Note: Either user OR email will be filled in, but not both. - user = ForeignKeyField(User, index=True, null=True) + user = QuayUserField(index=True, null=True) email = CharField(null=True) team = ForeignKeyField(Team, index=True) inviter = ForeignKeyField(User, related_name='inviter') @@ -156,7 +178,7 @@ class LoginService(BaseModel): class FederatedLogin(BaseModel): - user = ForeignKeyField(User, index=True) + user = QuayUserField(allows_robots=True, index=True) service = ForeignKeyField(LoginService, index=True) service_ident = CharField() metadata_json = TextField(default='{}') @@ -178,7 +200,7 @@ class Visibility(BaseModel): class Repository(BaseModel): - namespace_user = ForeignKeyField(User, null=True) + namespace_user = QuayUserField(null=True) name = CharField() visibility = ForeignKeyField(Visibility) description = TextField(null=True) @@ -199,7 +221,7 @@ class Role(BaseModel): class RepositoryPermission(BaseModel): team = ForeignKeyField(Team, index=True, null=True) - user = ForeignKeyField(User, index=True, null=True) + user = QuayUserField(allows_robots=True, index=True, null=True) repository = ForeignKeyField(Repository, index=True) role = ForeignKeyField(Role) @@ -213,12 +235,12 @@ class RepositoryPermission(BaseModel): class PermissionPrototype(BaseModel): - org = ForeignKeyField(User, index=True, related_name='orgpermissionproto') + org = QuayUserField(index=True, related_name='orgpermissionproto') uuid = CharField(default=uuid_generator) - activating_user = ForeignKeyField(User, index=True, null=True, - related_name='userpermissionproto') - delegate_user = ForeignKeyField(User, related_name='receivingpermission', - null=True) + activating_user = QuayUserField(allows_robots=True, index=True, null=True, + related_name='userpermissionproto') + delegate_user = QuayUserField(allows_robots=True,related_name='receivingpermission', + null=True) delegate_team = ForeignKeyField(Team, related_name='receivingpermission', null=True) role = ForeignKeyField(Role) @@ -249,16 +271,16 @@ class RepositoryBuildTrigger(BaseModel): uuid = CharField(default=uuid_generator) service = ForeignKeyField(BuildTriggerService, index=True) repository = ForeignKeyField(Repository, index=True) - connected_user = ForeignKeyField(User) + connected_user = QuayUserField() auth_token = CharField() config = TextField(default='{}') write_token = ForeignKeyField(AccessToken, null=True) - pull_robot = ForeignKeyField(User, null=True, related_name='triggerpullrobot') + pull_robot = QuayUserField(allows_robots=True, null=True, related_name='triggerpullrobot') class EmailConfirmation(BaseModel): code = CharField(default=random_string_generator(), unique=True, index=True) - user = ForeignKeyField(User) + user = QuayUserField() pw_reset = BooleanField(default=False) new_email = CharField(null=True) email_confirm = BooleanField(default=False) @@ -365,7 +387,7 @@ class RepositoryBuild(BaseModel): started = DateTimeField(default=datetime.now) display_name = CharField() trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True) - pull_robot = ForeignKeyField(User, null=True, related_name='buildpullrobot') + pull_robot = QuayUserField(null=True, related_name='buildpullrobot') logs_archived = BooleanField(default=False) @@ -384,9 +406,9 @@ class LogEntryKind(BaseModel): class LogEntry(BaseModel): kind = ForeignKeyField(LogEntryKind, index=True) - account = ForeignKeyField(User, index=True, related_name='account') - performer = ForeignKeyField(User, index=True, null=True, - related_name='performer') + account = QuayUserField(index=True, related_name='account') + performer = QuayUserField(allows_robots=True, index=True, null=True, + related_name='performer') repository = ForeignKeyField(Repository, index=True, null=True) access_token = ForeignKeyField(AccessToken, null=True) datetime = DateTimeField(default=datetime.now, index=True) @@ -399,7 +421,7 @@ class OAuthApplication(BaseModel): client_secret = CharField(default=random_string_generator(length=40)) redirect_uri = CharField() application_uri = CharField() - organization = ForeignKeyField(User) + organization = QuayUserField() name = CharField() description = TextField(default='') @@ -416,7 +438,7 @@ class OAuthAuthorizationCode(BaseModel): class OAuthAccessToken(BaseModel): uuid = CharField(default=uuid_generator, index=True) application = ForeignKeyField(OAuthApplication) - authorized_user = ForeignKeyField(User) + authorized_user = QuayUserField() scope = CharField() access_token = CharField(index=True) token_type = CharField(default='Bearer') @@ -432,7 +454,7 @@ class NotificationKind(BaseModel): class Notification(BaseModel): uuid = CharField(default=uuid_generator, index=True) kind = ForeignKeyField(NotificationKind, index=True) - target = ForeignKeyField(User, index=True) + target = QuayUserField(index=True) metadata_json = TextField(default='{}') created = DateTimeField(default=datetime.now, index=True) dismissed = BooleanField(default=False) diff --git a/data/model/legacy.py b/data/model/legacy.py index 34be41491..b7a20d1cd 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -14,7 +14,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite, DerivedImageStorage, ImageStorageTransformation, random_string_generator, - db, BUILD_PHASE) + db, BUILD_PHASE, QuayUserField) from peewee import JOIN_LEFT_OUTER, fn from util.validation import (validate_username, validate_email, validate_password, INVALID_PASSWORD_MESSAGE) @@ -288,6 +288,7 @@ def delete_robot(robot_username): try: robot = User.get(username=robot_username, robot=True) robot.delete_instance(recursive=True, delete_nullable=True) + except User.DoesNotExist: raise InvalidRobotException('Could not find robot with username: %s' % robot_username) diff --git a/initdb.py b/initdb.py index 20acf92b7..38f290ad4 100644 --- a/initdb.py +++ b/initdb.py @@ -156,6 +156,9 @@ def setup_database_for_testing(testcase): initialize_database() populate_database() + # Enable foreign key constraints. + model.db.obj.execute_sql('PRAGMA foreign_keys = ON;') + db_initialized_for_testing = True # Create a savepoint for the testcase. diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 1596c81c7..295030005 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -1780,7 +1780,7 @@ class TestOrgSubscription(ApiTestCase): class TestUserRobots(ApiTestCase): def getRobotNames(self): - return [r['name'] for r in self.getJsonResponse(UserRobotList)['robots']] + return [r['name'] for r in self.getJsonResponse(UserRobotList)['robots']] def test_robots(self): self.login(NO_ACCESS_USER) @@ -1834,6 +1834,65 @@ class TestOrgRobots(ApiTestCase): return [r['name'] for r in self.getJsonResponse(OrgRobotList, params=dict(orgname=ORGANIZATION))['robots']] + def test_delete_robot_after_use(self): + self.login(ADMIN_ACCESS_USER) + + # Create the robot. + self.putJsonResponse(OrgRobot, + params=dict(orgname=ORGANIZATION, robot_shortname='bender'), + expected_code=201) + + # Add the robot to a team. + membername = ORGANIZATION + '+bender' + self.putJsonResponse(TeamMember, + params=dict(orgname=ORGANIZATION, teamname='readers', + membername=membername)) + + # Add a repository permission. + self.putJsonResponse(RepositoryUserPermission, + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, username=membername), + data=dict(role='read')) + + # Add a permission prototype with the robot as the activating user. + self.postJsonResponse(PermissionPrototypeList, + params=dict(orgname=ORGANIZATION), + data=dict(role='read', + activating_user={'name': membername}, + delegate={'kind': 'user', + 'name': membername})) + + # Add a permission prototype with the robot as the delegating user. + self.postJsonResponse(PermissionPrototypeList, + params=dict(orgname=ORGANIZATION), + data=dict(role='read', + delegate={'kind': 'user', + 'name': membername})) + + # Add a build trigger with the robot as the pull robot. + database.BuildTriggerService.create(name='fakeservice') + + # Add a new fake trigger. + repo = model.get_repository(ORGANIZATION, ORG_REPO) + user = model.get_user(ADMIN_ACCESS_USER) + pull_robot = model.get_user(membername) + model.create_build_trigger(repo, 'fakeservice', 'sometoken', user, pull_robot=pull_robot) + + # Delete the robot and verify it works. + self.deleteResponse(OrgRobot, + params=dict(orgname=ORGANIZATION, robot_shortname='bender')) + + # All the above records should now be deleted, along with the robot. We verify a few of the + # critical ones below. + + # Check the team. + team = model.get_organization_team(ORGANIZATION, 'readers') + members = [member.username for member in model.get_organization_team_members(team.id)] + self.assertFalse(membername in members) + + # Check the robot itself. + self.assertIsNone(model.get_user(membername)) + + def test_robots(self): self.login(ADMIN_ACCESS_USER)