artifacts: handle detach/delete of artifacts for users who own them and are to be deleted.

- added relationships that cleanup properly
- cleanup code for detach/delete on users
This commit is contained in:
Marcin Kuzminski 2019-10-24 17:05:07 +02:00
parent 40960af53f
commit 8b8d8ab401
7 changed files with 165 additions and 54 deletions

View file

@ -517,7 +517,7 @@ class TestAdminUsersView(TestController):
route_path('user_delete', user_id=new_user.user_id),
params={'csrf_token': self.csrf_token})
assert_session_flash(response, 'Successfully deleted user')
assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
def test_delete_owner_of_repository(self, request, user_util):
self.log_user()

View file

@ -376,59 +376,69 @@ class UsersView(UserAppView):
_repos = c.user.repositories
_repo_groups = c.user.repository_groups
_user_groups = c.user.user_groups
_artifacts = c.user.artifacts
handle_repos = None
handle_repo_groups = None
handle_user_groups = None
# dummy call for flash of handle
set_handle_flash_repos = lambda: None
set_handle_flash_repo_groups = lambda: None
set_handle_flash_user_groups = lambda: None
handle_artifacts = None
# calls for flash of handle based on handle case detach or delete
def set_handle_flash_repos():
handle = handle_repos
if handle == 'detach':
h.flash(_('Detached %s repositories') % len(_repos),
category='success')
elif handle == 'delete':
h.flash(_('Deleted %s repositories') % len(_repos),
category='success')
def set_handle_flash_repo_groups():
handle = handle_repo_groups
if handle == 'detach':
h.flash(_('Detached %s repository groups') % len(_repo_groups),
category='success')
elif handle == 'delete':
h.flash(_('Deleted %s repository groups') % len(_repo_groups),
category='success')
def set_handle_flash_user_groups():
handle = handle_user_groups
if handle == 'detach':
h.flash(_('Detached %s user groups') % len(_user_groups),
category='success')
elif handle == 'delete':
h.flash(_('Deleted %s user groups') % len(_user_groups),
category='success')
def set_handle_flash_artifacts():
handle = handle_artifacts
if handle == 'detach':
h.flash(_('Detached %s artifacts') % len(_artifacts),
category='success')
elif handle == 'delete':
h.flash(_('Deleted %s artifacts') % len(_artifacts),
category='success')
if _repos and self.request.POST.get('user_repos'):
do = self.request.POST['user_repos']
if do == 'detach':
handle_repos = 'detach'
set_handle_flash_repos = lambda: h.flash(
_('Detached %s repositories') % len(_repos),
category='success')
elif do == 'delete':
handle_repos = 'delete'
set_handle_flash_repos = lambda: h.flash(
_('Deleted %s repositories') % len(_repos),
category='success')
handle_repos = self.request.POST['user_repos']
if _repo_groups and self.request.POST.get('user_repo_groups'):
do = self.request.POST['user_repo_groups']
if do == 'detach':
handle_repo_groups = 'detach'
set_handle_flash_repo_groups = lambda: h.flash(
_('Detached %s repository groups') % len(_repo_groups),
category='success')
elif do == 'delete':
handle_repo_groups = 'delete'
set_handle_flash_repo_groups = lambda: h.flash(
_('Deleted %s repository groups') % len(_repo_groups),
category='success')
handle_repo_groups = self.request.POST['user_repo_groups']
if _user_groups and self.request.POST.get('user_user_groups'):
do = self.request.POST['user_user_groups']
if do == 'detach':
handle_user_groups = 'detach'
set_handle_flash_user_groups = lambda: h.flash(
_('Detached %s user groups') % len(_user_groups),
category='success')
elif do == 'delete':
handle_user_groups = 'delete'
set_handle_flash_user_groups = lambda: h.flash(
_('Deleted %s user groups') % len(_user_groups),
category='success')
handle_user_groups = self.request.POST['user_user_groups']
if _artifacts and self.request.POST.get('user_artifacts'):
handle_artifacts = self.request.POST['user_artifacts']
old_values = c.user.get_api_data()
try:
UserModel().delete(c.user, handle_repos=handle_repos,
handle_repo_groups=handle_repo_groups,
handle_user_groups=handle_user_groups)
handle_user_groups=handle_user_groups,
handle_artifacts=handle_artifacts)
audit_logger.store_web(
'user.delete', action_data={'old_data': old_values},
@ -438,7 +448,9 @@ class UsersView(UserAppView):
set_handle_flash_repos()
set_handle_flash_repo_groups()
set_handle_flash_user_groups()
h.flash(_('Successfully deleted user'), category='success')
set_handle_flash_artifacts()
username = h.escape(old_values['username'])
h.flash(_('Successfully deleted user `{}`').format(username), category='success')
except (UserOwnsReposException, UserOwnsRepoGroupsException,
UserOwnsUserGroupsException, DefaultUserException) as e:
h.flash(e, category='warning')

View file

@ -58,6 +58,10 @@ class UserOwnsUserGroupsException(Exception):
pass
class UserOwnsArtifactsException(Exception):
pass
class UserGroupAssignedException(Exception):
pass

View file

@ -617,13 +617,19 @@ class User(Base, BaseModel):
# user pull requests
user_pull_requests = relationship('PullRequest', cascade='all')
# external identities
extenal_identities = relationship(
external_identities = relationship(
'ExternalIdentity',
primaryjoin="User.user_id==ExternalIdentity.local_user_id",
cascade='all')
# review rules
user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
# artifacts owned
artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
# no cascade, set NULL
scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
def __unicode__(self):
return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
self.user_id, self.username)
@ -1704,7 +1710,8 @@ class Repository(Base, BaseModel):
scoped_tokens = relationship('UserApiKeys', cascade="all")
artifacts = relationship('FileStore', cascade="all")
# no cascade, set NULL
artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
def __unicode__(self):
return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
@ -2579,6 +2586,9 @@ class RepoGroup(Base, BaseModel):
user = relationship('User')
integrations = relationship('Integration', cascade="all, delete-orphan")
# no cascade, set NULL
scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
def __init__(self, group_name='', parent_group=None):
self.group_name = group_name
self.parent_group = parent_group
@ -3870,6 +3880,7 @@ class _SetState(object):
log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
raise
class _PullRequestBase(BaseModel):
"""
Common attributes of pull request and version entries.
@ -4148,14 +4159,10 @@ class PullRequest(Base, _PullRequestBase):
else:
return '<DB:PullRequest at %#x>' % id(self)
reviewers = relationship('PullRequestReviewers',
cascade="all, delete-orphan")
statuses = relationship('ChangesetStatus',
cascade="all, delete-orphan")
comments = relationship('ChangesetComment',
cascade="all, delete-orphan")
versions = relationship('PullRequestVersion',
cascade="all, delete-orphan",
reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
comments = relationship('ChangesetComment', cascade="all, delete-orphan")
versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
lazy='dynamic')
@classmethod

View file

@ -37,7 +37,7 @@ from rhodecode.lib.utils2 import (
AttributeDict, str2bool)
from rhodecode.lib.exceptions import (
DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
UserOwnsUserGroupsException, NotAllowedToCreateUserError)
UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
from rhodecode.lib.caching_query import FromCache
from rhodecode.model import BaseModel
from rhodecode.model.auth_token import AuthTokenModel
@ -505,8 +505,33 @@ class UserModel(BaseModel):
# if nothing is done we have left overs left
return left_overs
def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
_superadmin = self.cls.get_first_super_admin()
left_overs = True
if handle_mode == 'detach':
for a in artifacts:
a.upload_user = _superadmin
# set description we know why we super admin now owns
# additional artifacts that were orphaned !
a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
self.sa.add(a)
left_overs = False
elif handle_mode == 'delete':
from rhodecode.apps.file_store import utils as store_utils
storage = store_utils.get_file_storage(self.request.registry.settings)
for a in artifacts:
file_uid = a.file_uid
storage.delete(file_uid)
self.sa.delete(a)
left_overs = False
# if nothing is done we have left overs left
return left_overs
def delete(self, user, cur_user=None, handle_repos=None,
handle_repo_groups=None, handle_user_groups=None):
handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
from rhodecode.lib.hooks_base import log_delete_user
if not cur_user:
@ -548,6 +573,15 @@ class UserModel(BaseModel):
u'removed. Switch owners or remove those user groups:%s'
% (user.username, len(user_groups), ', '.join(user_groups)))
left_overs = self._handle_user_artifacts(
user.username, user.artifacts, handle_artifacts)
if left_overs and user.artifacts:
artifacts = [x.file_uid for x in user.artifacts]
raise UserOwnsArtifactsException(
u'user "%s" still owns %s artifacts and cannot be '
u'removed. Switch owners or remove those artifacts:%s'
% (user.username, len(artifacts), ', '.join(artifacts)))
user_data = user.get_dict() # fetch user data before expire
# we might change the user data with detach/delete, make sure

View file

@ -13,6 +13,8 @@
(_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
(_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
(_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]),
(_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
(_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
@ -23,10 +25,19 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
<h3 class="panel-title">${_('User: {}').format(c.user.username)}</h3>
</div>
<div class="panel-body">
${base.dt_info_panel(elems)}
<table class="rctable">
<tr>
<th>Name</th>
<th>Value</th>
<th>Action</th>
</tr>
% for elem in elems:
${base.tr_info_entry(elem)}
% endfor
</table>
</div>
</div>
@ -132,6 +143,19 @@
<input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_2">${_('Delete repositories')}</label>
</td>
</tr>
<tr>
<td>
${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
</td>
<td>
<input type="radio" id="user_artifacts_1" name="user_artifacts" value="detach" checked="checked" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_1">${_('Detach Artifacts')}</label>
</td>
<td>
<input type="radio" id="user_artifacts_2" name="user_artifacts" value="delete" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_2">${_('Delete Artifacts')}</label>
</td>
</tr>
</table>
<div style="margin: 0 0 20px 0" class="fake-space"></div>
<div class="pull-left">

View file

@ -164,6 +164,36 @@
</dl>
</%def>
<%def name="tr_info_entry(element)">
<% key, val, title, show_items = element %>
<tr>
<td style="vertical-align: top">${key}</td>
<td title="${h.tooltip(title)}">
%if callable(val):
## allow lazy evaluation of elements
${val()}
%else:
${val}
%endif
%if show_items:
<div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
% for item in show_items:
<dt></dt>
<dd>${item}</dd>
% endfor
</div>
%endif
</td>
<td style="vertical-align: top">
%if show_items:
<span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
%endif
</td>
</tr>
</%def>
<%def name="gravatar(email, size=16)">
<%
if (size > 16):