Add support for deleting namespaces (users, organizations)
Fixes #102 Fixes #105
This commit is contained in:
parent
a74e94fb67
commit
73eb66eac5
23 changed files with 407 additions and 33 deletions
|
@ -359,8 +359,15 @@ class User(BaseModel):
|
|||
raise RuntimeError('Non-recursive delete on user.')
|
||||
|
||||
# These models don't need to use transitive deletes, because the referenced objects
|
||||
# are cleaned up directly
|
||||
skip_transitive_deletes = {Image}
|
||||
# are cleaned up directly in the model.
|
||||
skip_transitive_deletes = {Image, Repository, Team, RepositoryBuild, ServiceKeyApproval,
|
||||
RepositoryBuildTrigger, ServiceKey, RepositoryPermission,
|
||||
TeamMemberInvite, Star, RepositoryAuthorizedEmail, TeamMember,
|
||||
RepositoryTag, PermissionPrototype, DerivedStorageForImage,
|
||||
TagManifest, AccessToken, OAuthAccessToken, BlobUpload,
|
||||
RepositoryNotification, OAuthAuthorizationCode,
|
||||
RepositoryActionCount, TagManifestLabel}
|
||||
|
||||
delete_instance_filtered(self, User, delete_nullable, skip_transitive_deletes)
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ from datetime import datetime, timedelta
|
|||
from data.database import (User, LoginService, FederatedLogin, RepositoryPermission, TeamMember,
|
||||
Team, Repository, TupleSelector, TeamRole, Namespace, Visibility,
|
||||
EmailConfirmation, Role, db_for_update, random_string_generator,
|
||||
UserRegion, ImageStorageLocation)
|
||||
UserRegion, ImageStorageLocation, QueueItem, TeamMemberInvite,
|
||||
ServiceKeyApproval, OAuthApplication, RepositoryBuildTrigger)
|
||||
from data.model import (DataModelException, InvalidPasswordException, InvalidRobotException,
|
||||
InvalidUsernameException, InvalidEmailAddressException,
|
||||
TooManyUsersException, TooManyLoginAttemptsException, db_transaction,
|
||||
|
@ -657,11 +658,81 @@ def detach_external_login(user, service_name):
|
|||
FederatedLogin.service == service).execute()
|
||||
|
||||
|
||||
def delete_user(user):
|
||||
def get_solely_admined_organizations(user_obj):
|
||||
""" Returns the organizations admined solely by the given user. """
|
||||
orgs = (User.select()
|
||||
.where(User.organization == True)
|
||||
.join(Team)
|
||||
.join(TeamRole)
|
||||
.where(TeamRole.name == 'admin')
|
||||
.switch(Team)
|
||||
.join(TeamMember)
|
||||
.where(TeamMember.user == user_obj)
|
||||
.distinct())
|
||||
|
||||
# Filter to organizations where the user is the sole admin.
|
||||
solely_admined = []
|
||||
for org in orgs:
|
||||
admin_user_count = (TeamMember.select()
|
||||
.join(Team)
|
||||
.join(TeamRole)
|
||||
.where(Team.organization == org, TeamRole.name == 'admin')
|
||||
.switch(TeamMember)
|
||||
.join(User)
|
||||
.where(User.robot == False)
|
||||
.distinct()
|
||||
.count())
|
||||
|
||||
if admin_user_count == 1:
|
||||
solely_admined.append(org)
|
||||
|
||||
return solely_admined
|
||||
|
||||
|
||||
def delete_user(user, queues, force=False):
|
||||
if not force and not user.organization:
|
||||
# Ensure that the user is not the sole admin for any organizations. If so, then the user
|
||||
# cannot be deleted before those organizations are deleted or reassigned.
|
||||
organizations = get_solely_admined_organizations(user)
|
||||
if len(organizations) > 0:
|
||||
message = 'Cannot delete %s as you are the only admin for organizations: ' % user.username
|
||||
for index, org in enumerate(organizations):
|
||||
if index > 0:
|
||||
message = message + ', '
|
||||
|
||||
message = message + org.username
|
||||
|
||||
raise DataModelException(message)
|
||||
|
||||
# Delete all queue items for the user.
|
||||
for queue in queues:
|
||||
queue.delete_namespaced_items(user.username)
|
||||
|
||||
# Delete any repositories under the user's namespace.
|
||||
for repo in list(Repository.select().where(Repository.namespace_user == user)):
|
||||
repository.purge_repository(user.username, repo.name)
|
||||
|
||||
if user.organization:
|
||||
# Delete the organization's teams.
|
||||
for team in Team.select().where(Team.organization == user):
|
||||
team.delete_instance(recursive=True)
|
||||
|
||||
# Delete any OAuth approvals and tokens associated with the user.
|
||||
for app in OAuthApplication.select().where(OAuthApplication.organization == user):
|
||||
app.delete_instance(recursive=True)
|
||||
else:
|
||||
# Remove the user from any teams in which they are a member.
|
||||
TeamMember.delete().where(TeamMember.user == user).execute()
|
||||
|
||||
# Delete any repository buildtriggers where the user is the connected user.
|
||||
triggers = RepositoryBuildTrigger.select().where(RepositoryBuildTrigger.connected_user == user)
|
||||
for trigger in triggers:
|
||||
trigger.delete_instance(recursive=True, delete_nullable=False)
|
||||
|
||||
# Null out any service key approvals. We technically lose information here, but its better than
|
||||
# falling and only occurs if a superuser is being deleted.
|
||||
ServiceKeyApproval.update(approver=None).where(ServiceKeyApproval.approver == user).execute()
|
||||
|
||||
# Delete the user itself.
|
||||
user.delete_instance(recursive=True, delete_nullable=True)
|
||||
|
||||
|
|
|
@ -33,12 +33,14 @@ class BuildMetricQueueReporter(object):
|
|||
class WorkQueue(object):
|
||||
""" Work queue defines methods for interacting with a queue backed by the database. """
|
||||
def __init__(self, queue_name, transaction_factory,
|
||||
canonical_name_match_list=None, reporter=None, metric_queue=None):
|
||||
canonical_name_match_list=None, reporter=None, metric_queue=None,
|
||||
has_namespace=False):
|
||||
self._queue_name = queue_name
|
||||
self._reporter = reporter
|
||||
self._metric_queue = metric_queue
|
||||
self._transaction_factory = transaction_factory
|
||||
self._currently_processing = False
|
||||
self._has_namespaced_items = has_namespace
|
||||
|
||||
if canonical_name_match_list is None:
|
||||
self._canonical_name_match_list = []
|
||||
|
@ -130,6 +132,15 @@ class WorkQueue(object):
|
|||
except QueueItem.DoesNotExist:
|
||||
return False
|
||||
|
||||
def delete_namespaced_items(self, namespace, subpath=None):
|
||||
""" Deletes all items in this queue that exist under the given namespace. """
|
||||
if not self._has_namespaced_items:
|
||||
return False
|
||||
|
||||
subpath_query = '%s/' % subpath if subpath else ''
|
||||
queue_prefix = '%s/%s/%s%%' % (self._queue_name, namespace, subpath_query)
|
||||
QueueItem.delete().where(QueueItem.queue_name ** queue_prefix).execute()
|
||||
|
||||
def put(self, canonical_name_list, message, available_after=0, retries_remaining=5):
|
||||
"""
|
||||
Put an item, if it shouldn't be processed for some number of seconds,
|
||||
|
|
Reference in a new issue