Fix issue where we didn't delete robots immediately under a namespace
This could result in "hanging" robot accounts, although that would only leak the names of said accounts. Now we delete them immediately AND we proactively delete them before replacing the namespace (just to be sure)
This commit is contained in:
parent
bd1f3e6bb8
commit
f06eec8a35
5 changed files with 132 additions and 10 deletions
23
data/migrations/versions/5b7503aada1b_cleanup_old_robots.py
Normal file
23
data/migrations/versions/5b7503aada1b_cleanup_old_robots.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""Cleanup old robots
|
||||
|
||||
Revision ID: 5b7503aada1b
|
||||
Revises: 224ce4c72c2f
|
||||
Create Date: 2018-05-09 17:18:52.230504
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5b7503aada1b'
|
||||
down_revision = '224ce4c72c2f'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from util.migrate.cleanup_old_robots import cleanup_old_robots
|
||||
|
||||
def upgrade(tables):
|
||||
cleanup_old_robots()
|
||||
|
||||
def downgrade(tables):
|
||||
# Nothing to do.
|
||||
pass
|
|
@ -7,6 +7,8 @@ from mock import patch
|
|||
from data.database import EmailConfirmation, User, DeletedNamespace
|
||||
from data.model.user import create_user_noverify, validate_reset_code, get_active_users
|
||||
from data.model.user import mark_namespace_for_deletion, delete_namespace_via_marker
|
||||
from data.model.user import create_robot, lookup_robot, list_namespace_robots
|
||||
from data.model.user import InvalidRobotException
|
||||
from util.timedeltastring import convert_to_timedelta
|
||||
from data.queue import WorkQueue
|
||||
from test.fixtures import *
|
||||
|
@ -48,12 +50,30 @@ def test_mark_namespace_for_deletion(initialized_db):
|
|||
# Create a user and then mark it for deletion.
|
||||
user = create_user_noverify('foobar', 'foo@example.com', email_required=False)
|
||||
|
||||
# Add some robots.
|
||||
create_robot('foo', user)
|
||||
create_robot('bar', user)
|
||||
|
||||
assert lookup_robot('foobar+foo') is not None
|
||||
assert lookup_robot('foobar+bar') is not None
|
||||
assert len(list(list_namespace_robots('foobar'))) == 2
|
||||
|
||||
# Mark the user for deletion.
|
||||
queue = WorkQueue('testgcnamespace', create_transaction)
|
||||
mark_namespace_for_deletion(user, [], queue)
|
||||
|
||||
# Ensure the older user is still in the DB.
|
||||
assert User.get(id=user.id).username != 'foobar'
|
||||
older_user = User.get(id=user.id)
|
||||
assert older_user.username != 'foobar'
|
||||
|
||||
# Ensure the robots are deleted.
|
||||
with pytest.raises(InvalidRobotException):
|
||||
assert lookup_robot('foobar+foo')
|
||||
|
||||
with pytest.raises(InvalidRobotException):
|
||||
assert lookup_robot('foobar+bar')
|
||||
|
||||
assert len(list(list_namespace_robots(older_user.username))) == 0
|
||||
|
||||
# Ensure we can create a user with the same namespace again.
|
||||
new_user = create_user_noverify('foobar', 'foo@example.com', email_required=False)
|
||||
|
|
|
@ -68,7 +68,6 @@ def create_user_noverify(username, email, email_required=True, prompts=tuple(),
|
|||
|
||||
try:
|
||||
existing = User.get((User.username == username) | (User.email == email))
|
||||
|
||||
logger.info('Existing user with same username or email.')
|
||||
|
||||
# A user already exists with either the same username or email
|
||||
|
@ -77,11 +76,11 @@ def create_user_noverify(username, email, email_required=True, prompts=tuple(),
|
|||
username)
|
||||
raise InvalidEmailAddressException('Email has already been used: %s' %
|
||||
email)
|
||||
|
||||
except User.DoesNotExist:
|
||||
# This is actually the happy path
|
||||
logger.debug('Email and username are unique!')
|
||||
|
||||
# Create the user.
|
||||
try:
|
||||
default_expr_s = _convert_to_s(config.app_config['DEFAULT_TAG_EXPIRATION'])
|
||||
default_max_builds = config.app_config.get('DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT')
|
||||
|
@ -970,10 +969,12 @@ def delete_user(user, queues):
|
|||
def _delete_user_linked_data(user):
|
||||
if user.organization:
|
||||
# Delete the organization's teams.
|
||||
with db_transaction():
|
||||
for team in Team.select().where(Team.organization == user):
|
||||
team.delete_instance(recursive=True)
|
||||
|
||||
# Delete any OAuth approvals and tokens associated with the user.
|
||||
with db_transaction():
|
||||
for app in OAuthApplication.select().where(OAuthApplication.organization == user):
|
||||
app.delete_instance(recursive=True)
|
||||
else:
|
||||
|
@ -981,10 +982,17 @@ def _delete_user_linked_data(user):
|
|||
TeamMember.delete().where(TeamMember.user == user).execute()
|
||||
|
||||
# Delete any repository buildtriggers where the user is the connected user.
|
||||
with db_transaction():
|
||||
triggers = RepositoryBuildTrigger.select().where(RepositoryBuildTrigger.connected_user == user)
|
||||
for trigger in triggers:
|
||||
trigger.delete_instance(recursive=True, delete_nullable=False)
|
||||
|
||||
# Delete any robots owned by this user.
|
||||
with db_transaction():
|
||||
robots = list(list_namespace_robots(user.username))
|
||||
for robot in robots:
|
||||
robot.delete_instance(recursive=True, delete_nullable=True)
|
||||
|
||||
# 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()
|
||||
|
|
28
util/migrate/cleanup_old_robots.py
Normal file
28
util/migrate/cleanup_old_robots.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from data.database import User
|
||||
from util.names import parse_robot_username
|
||||
|
||||
def cleanup_old_robots(page_size=50):
|
||||
""" Deletes any robots that live under namespaces that no longer exist. """
|
||||
# Collect the robot accounts to delete.
|
||||
page_number = 1
|
||||
to_delete = []
|
||||
|
||||
while True:
|
||||
found_bots = False
|
||||
for robot in list(User.select().where(User.robot == True).paginate(page_number, page_size)):
|
||||
found_bots = True
|
||||
namespace, _ = parse_robot_username(robot.username)
|
||||
try:
|
||||
User.get(username=namespace)
|
||||
except User.DoesNotExist:
|
||||
# Save the robot account for deletion.
|
||||
to_delete.append(robot)
|
||||
|
||||
if not found_bots:
|
||||
break
|
||||
|
||||
page_number = page_number + 1
|
||||
|
||||
# Cleanup any robot accounts whose corresponding namespace doesn't exist.
|
||||
for robot in to_delete:
|
||||
robot.delete_instance(recursive=True, delete_nullable=True)
|
43
util/migrate/test/test_cleanup_old_robots.py
Normal file
43
util/migrate/test/test_cleanup_old_robots.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import pytest
|
||||
|
||||
from data.database import User
|
||||
from util.migrate.cleanup_old_robots import cleanup_old_robots
|
||||
|
||||
from test.fixtures import *
|
||||
|
||||
def test_cleanup_old_robots(initialized_db):
|
||||
before_robot_count = User.select().where(User.robot == True).count()
|
||||
before_user_count = User.select().count()
|
||||
|
||||
# Run the cleanup once, and ensure it does nothing.
|
||||
cleanup_old_robots()
|
||||
|
||||
after_robot_count = User.select().where(User.robot == True).count()
|
||||
after_user_count = User.select().count()
|
||||
|
||||
assert before_robot_count == after_robot_count
|
||||
assert before_user_count == after_user_count
|
||||
|
||||
# Create some orphan robots.
|
||||
created = set()
|
||||
for index in range(0, 50):
|
||||
created.add('doesnotexist+a%s' % index)
|
||||
created.add('anothernamespace+b%s' % index)
|
||||
|
||||
User.create(username='doesnotexist+a%s' % index, robot=True)
|
||||
User.create(username='anothernamespace+b%s' % index, robot=True)
|
||||
|
||||
before_robot_count = User.select().where(User.robot == True).count()
|
||||
before_user_count = User.select().count()
|
||||
|
||||
cleanup_old_robots(page_size=10)
|
||||
|
||||
after_robot_count = User.select().where(User.robot == True).count()
|
||||
after_user_count = User.select().count()
|
||||
|
||||
assert before_robot_count == after_robot_count + len(created)
|
||||
assert before_user_count == after_user_count + len(created)
|
||||
|
||||
for name in created:
|
||||
with pytest.raises(User.DoesNotExist):
|
||||
User.get(username=name)
|
Reference in a new issue