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:
parent
40960af53f
commit
8b8d8ab401
7 changed files with 165 additions and 54 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ class UserOwnsUserGroupsException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class UserOwnsArtifactsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserGroupAssignedException(Exception):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue