- Turn on foreign key constraint checking in the tests
- Change all ForeignKeyField's that refer to users to use our custom class, and mark those that allow robots - Change robot delete to only execute the subset of queries necessary to actually delete robots
This commit is contained in:
parent
8548538516
commit
158acd4f41
4 changed files with 104 additions and 24 deletions
|
@ -90,6 +90,15 @@ def close_db_filter(_):
|
||||||
read_slave.close()
|
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 BaseModel(ReadSlaveModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
@ -116,7 +125,7 @@ class TeamRole(BaseModel):
|
||||||
|
|
||||||
class Team(BaseModel):
|
class Team(BaseModel):
|
||||||
name = CharField(index=True)
|
name = CharField(index=True)
|
||||||
organization = ForeignKeyField(User, index=True)
|
organization = QuayUserField(index=True)
|
||||||
role = ForeignKeyField(TeamRole)
|
role = ForeignKeyField(TeamRole)
|
||||||
description = TextField(default='')
|
description = TextField(default='')
|
||||||
|
|
||||||
|
@ -130,7 +139,7 @@ class Team(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(BaseModel):
|
class TeamMember(BaseModel):
|
||||||
user = ForeignKeyField(User, index=True)
|
user = QuayUserField(allows_robots=True, index=True)
|
||||||
team = ForeignKeyField(Team, index=True)
|
team = ForeignKeyField(Team, index=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -144,7 +153,7 @@ class TeamMember(BaseModel):
|
||||||
|
|
||||||
class TeamMemberInvite(BaseModel):
|
class TeamMemberInvite(BaseModel):
|
||||||
# Note: Either user OR email will be filled in, but not both.
|
# 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)
|
email = CharField(null=True)
|
||||||
team = ForeignKeyField(Team, index=True)
|
team = ForeignKeyField(Team, index=True)
|
||||||
inviter = ForeignKeyField(User, related_name='inviter')
|
inviter = ForeignKeyField(User, related_name='inviter')
|
||||||
|
@ -156,7 +165,7 @@ class LoginService(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class FederatedLogin(BaseModel):
|
class FederatedLogin(BaseModel):
|
||||||
user = ForeignKeyField(User, index=True)
|
user = QuayUserField(allows_robots=True, index=True)
|
||||||
service = ForeignKeyField(LoginService, index=True)
|
service = ForeignKeyField(LoginService, index=True)
|
||||||
service_ident = CharField()
|
service_ident = CharField()
|
||||||
metadata_json = TextField(default='{}')
|
metadata_json = TextField(default='{}')
|
||||||
|
@ -178,7 +187,7 @@ class Visibility(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Repository(BaseModel):
|
class Repository(BaseModel):
|
||||||
namespace_user = ForeignKeyField(User, null=True)
|
namespace_user = QuayUserField(null=True)
|
||||||
name = CharField()
|
name = CharField()
|
||||||
visibility = ForeignKeyField(Visibility)
|
visibility = ForeignKeyField(Visibility)
|
||||||
description = TextField(null=True)
|
description = TextField(null=True)
|
||||||
|
@ -199,7 +208,7 @@ class Role(BaseModel):
|
||||||
|
|
||||||
class RepositoryPermission(BaseModel):
|
class RepositoryPermission(BaseModel):
|
||||||
team = ForeignKeyField(Team, index=True, null=True)
|
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)
|
repository = ForeignKeyField(Repository, index=True)
|
||||||
role = ForeignKeyField(Role)
|
role = ForeignKeyField(Role)
|
||||||
|
|
||||||
|
@ -213,12 +222,12 @@ class RepositoryPermission(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class PermissionPrototype(BaseModel):
|
class PermissionPrototype(BaseModel):
|
||||||
org = ForeignKeyField(User, index=True, related_name='orgpermissionproto')
|
org = QuayUserField(index=True, related_name='orgpermissionproto')
|
||||||
uuid = CharField(default=uuid_generator)
|
uuid = CharField(default=uuid_generator)
|
||||||
activating_user = ForeignKeyField(User, index=True, null=True,
|
activating_user = QuayUserField(allows_robots=True, index=True, null=True,
|
||||||
related_name='userpermissionproto')
|
related_name='userpermissionproto')
|
||||||
delegate_user = ForeignKeyField(User, related_name='receivingpermission',
|
delegate_user = QuayUserField(allows_robots=True,related_name='receivingpermission',
|
||||||
null=True)
|
null=True)
|
||||||
delegate_team = ForeignKeyField(Team, related_name='receivingpermission',
|
delegate_team = ForeignKeyField(Team, related_name='receivingpermission',
|
||||||
null=True)
|
null=True)
|
||||||
role = ForeignKeyField(Role)
|
role = ForeignKeyField(Role)
|
||||||
|
@ -249,16 +258,16 @@ class RepositoryBuildTrigger(BaseModel):
|
||||||
uuid = CharField(default=uuid_generator)
|
uuid = CharField(default=uuid_generator)
|
||||||
service = ForeignKeyField(BuildTriggerService, index=True)
|
service = ForeignKeyField(BuildTriggerService, index=True)
|
||||||
repository = ForeignKeyField(Repository, index=True)
|
repository = ForeignKeyField(Repository, index=True)
|
||||||
connected_user = ForeignKeyField(User)
|
connected_user = QuayUserField()
|
||||||
auth_token = CharField()
|
auth_token = CharField()
|
||||||
config = TextField(default='{}')
|
config = TextField(default='{}')
|
||||||
write_token = ForeignKeyField(AccessToken, null=True)
|
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):
|
class EmailConfirmation(BaseModel):
|
||||||
code = CharField(default=random_string_generator(), unique=True, index=True)
|
code = CharField(default=random_string_generator(), unique=True, index=True)
|
||||||
user = ForeignKeyField(User)
|
user = QuayUserField()
|
||||||
pw_reset = BooleanField(default=False)
|
pw_reset = BooleanField(default=False)
|
||||||
new_email = CharField(null=True)
|
new_email = CharField(null=True)
|
||||||
email_confirm = BooleanField(default=False)
|
email_confirm = BooleanField(default=False)
|
||||||
|
@ -365,7 +374,7 @@ class RepositoryBuild(BaseModel):
|
||||||
started = DateTimeField(default=datetime.now)
|
started = DateTimeField(default=datetime.now)
|
||||||
display_name = CharField()
|
display_name = CharField()
|
||||||
trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True)
|
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)
|
logs_archived = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -384,9 +393,9 @@ class LogEntryKind(BaseModel):
|
||||||
|
|
||||||
class LogEntry(BaseModel):
|
class LogEntry(BaseModel):
|
||||||
kind = ForeignKeyField(LogEntryKind, index=True)
|
kind = ForeignKeyField(LogEntryKind, index=True)
|
||||||
account = ForeignKeyField(User, index=True, related_name='account')
|
account = QuayUserField(index=True, related_name='account')
|
||||||
performer = ForeignKeyField(User, index=True, null=True,
|
performer = QuayUserField(allows_robots=True, index=True, null=True,
|
||||||
related_name='performer')
|
related_name='performer')
|
||||||
repository = ForeignKeyField(Repository, index=True, null=True)
|
repository = ForeignKeyField(Repository, index=True, null=True)
|
||||||
access_token = ForeignKeyField(AccessToken, null=True)
|
access_token = ForeignKeyField(AccessToken, null=True)
|
||||||
datetime = DateTimeField(default=datetime.now, index=True)
|
datetime = DateTimeField(default=datetime.now, index=True)
|
||||||
|
@ -399,7 +408,7 @@ class OAuthApplication(BaseModel):
|
||||||
client_secret = CharField(default=random_string_generator(length=40))
|
client_secret = CharField(default=random_string_generator(length=40))
|
||||||
redirect_uri = CharField()
|
redirect_uri = CharField()
|
||||||
application_uri = CharField()
|
application_uri = CharField()
|
||||||
organization = ForeignKeyField(User)
|
organization = QuayUserField()
|
||||||
|
|
||||||
name = CharField()
|
name = CharField()
|
||||||
description = TextField(default='')
|
description = TextField(default='')
|
||||||
|
@ -416,7 +425,7 @@ class OAuthAuthorizationCode(BaseModel):
|
||||||
class OAuthAccessToken(BaseModel):
|
class OAuthAccessToken(BaseModel):
|
||||||
uuid = CharField(default=uuid_generator, index=True)
|
uuid = CharField(default=uuid_generator, index=True)
|
||||||
application = ForeignKeyField(OAuthApplication)
|
application = ForeignKeyField(OAuthApplication)
|
||||||
authorized_user = ForeignKeyField(User)
|
authorized_user = QuayUserField()
|
||||||
scope = CharField()
|
scope = CharField()
|
||||||
access_token = CharField(index=True)
|
access_token = CharField(index=True)
|
||||||
token_type = CharField(default='Bearer')
|
token_type = CharField(default='Bearer')
|
||||||
|
@ -432,7 +441,7 @@ class NotificationKind(BaseModel):
|
||||||
class Notification(BaseModel):
|
class Notification(BaseModel):
|
||||||
uuid = CharField(default=uuid_generator, index=True)
|
uuid = CharField(default=uuid_generator, index=True)
|
||||||
kind = ForeignKeyField(NotificationKind, index=True)
|
kind = ForeignKeyField(NotificationKind, index=True)
|
||||||
target = ForeignKeyField(User, index=True)
|
target = QuayUserField(index=True)
|
||||||
metadata_json = TextField(default='{}')
|
metadata_json = TextField(default='{}')
|
||||||
created = DateTimeField(default=datetime.now, index=True)
|
created = DateTimeField(default=datetime.now, index=True)
|
||||||
dismissed = BooleanField(default=False)
|
dismissed = BooleanField(default=False)
|
||||||
|
|
|
@ -14,7 +14,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor
|
||||||
ExternalNotificationEvent, ExternalNotificationMethod,
|
ExternalNotificationEvent, ExternalNotificationMethod,
|
||||||
RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite,
|
RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite,
|
||||||
DerivedImageStorage, ImageStorageTransformation, random_string_generator,
|
DerivedImageStorage, ImageStorageTransformation, random_string_generator,
|
||||||
db, BUILD_PHASE)
|
db, BUILD_PHASE, QuayUserField)
|
||||||
from peewee import JOIN_LEFT_OUTER, fn
|
from peewee import JOIN_LEFT_OUTER, fn
|
||||||
from util.validation import (validate_username, validate_email, validate_password,
|
from util.validation import (validate_username, validate_email, validate_password,
|
||||||
INVALID_PASSWORD_MESSAGE)
|
INVALID_PASSWORD_MESSAGE)
|
||||||
|
@ -287,7 +287,16 @@ def regenerate_robot_token(robot_shortname, parent):
|
||||||
def delete_robot(robot_username):
|
def delete_robot(robot_username):
|
||||||
try:
|
try:
|
||||||
robot = User.get(username=robot_username, robot=True)
|
robot = User.get(username=robot_username, robot=True)
|
||||||
robot.delete_instance(recursive=True, delete_nullable=True)
|
|
||||||
|
# For all the model dependencies, only delete those that allow robots.
|
||||||
|
for query, fk in robot.dependencies(search_nullable=True):
|
||||||
|
if isinstance(fk, QuayUserField) and fk.allows_robots:
|
||||||
|
model = fk.model_class
|
||||||
|
model.delete().where(query).execute()
|
||||||
|
|
||||||
|
# Delete the robot itself.
|
||||||
|
robot.delete_instance(recursive=False)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
raise InvalidRobotException('Could not find robot with username: %s' %
|
raise InvalidRobotException('Could not find robot with username: %s' %
|
||||||
robot_username)
|
robot_username)
|
||||||
|
|
|
@ -156,6 +156,9 @@ def setup_database_for_testing(testcase):
|
||||||
initialize_database()
|
initialize_database()
|
||||||
populate_database()
|
populate_database()
|
||||||
|
|
||||||
|
# Enable foreign key constraints.
|
||||||
|
model.db.obj.execute_sql('PRAGMA foreign_keys = ON;')
|
||||||
|
|
||||||
db_initialized_for_testing = True
|
db_initialized_for_testing = True
|
||||||
|
|
||||||
# Create a savepoint for the testcase.
|
# Create a savepoint for the testcase.
|
||||||
|
|
|
@ -1834,6 +1834,65 @@ class TestOrgRobots(ApiTestCase):
|
||||||
return [r['name'] for r in self.getJsonResponse(OrgRobotList,
|
return [r['name'] for r in self.getJsonResponse(OrgRobotList,
|
||||||
params=dict(orgname=ORGANIZATION))['robots']]
|
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):
|
def test_robots(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
|
Reference in a new issue