users: added option to detach pull requests for users which we delete.

- as bonus added ability to specify new owner via ?detach_user_id=NUM GET param.
This commit is contained in:
Daniel Dourvaris 2020-05-20 12:33:43 +02:00
parent 0937ed89a1
commit 03f066ce9a
6 changed files with 135 additions and 39 deletions

View file

@ -39,7 +39,8 @@ from rhodecode.model.db import true, UserNotice
from rhodecode.lib import audit_logger, rc_cache
from rhodecode.lib.exceptions import (
UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
UserOwnsUserGroupsException, DefaultUserException)
UserOwnsUserGroupsException, UserOwnsPullRequestsException,
UserOwnsArtifactsException, DefaultUserException)
from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import (
LoginRequired, HasPermissionAllDecorator, CSRFRequired)
@ -377,11 +378,13 @@ class UsersView(UserAppView):
_repos = c.user.repositories
_repo_groups = c.user.repository_groups
_user_groups = c.user.user_groups
_pull_requests = c.user.user_pull_requests
_artifacts = c.user.artifacts
handle_repos = None
handle_repo_groups = None
handle_user_groups = None
handle_pull_requests = None
handle_artifacts = None
# calls for flash of handle based on handle case detach or delete
@ -412,6 +415,15 @@ class UsersView(UserAppView):
h.flash(_('Deleted %s user groups') % len(_user_groups),
category='success')
def set_handle_flash_pull_requests():
handle = handle_pull_requests
if handle == 'detach':
h.flash(_('Detached %s pull requests') % len(_pull_requests),
category='success')
elif handle == 'delete':
h.flash(_('Deleted %s pull requests') % len(_pull_requests),
category='success')
def set_handle_flash_artifacts():
handle = handle_artifacts
if handle == 'detach':
@ -421,6 +433,12 @@ class UsersView(UserAppView):
h.flash(_('Deleted %s artifacts') % len(_artifacts),
category='success')
handle_user = User.get_first_super_admin()
handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
if handle_user_id:
# NOTE(marcink): we get new owner for objects...
handle_user = User.get_or_404(handle_user_id)
if _repos and self.request.POST.get('user_repos'):
handle_repos = self.request.POST['user_repos']
@ -430,16 +448,25 @@ class UsersView(UserAppView):
if _user_groups and self.request.POST.get('user_user_groups'):
handle_user_groups = self.request.POST['user_user_groups']
if _pull_requests and self.request.POST.get('user_pull_requests'):
handle_pull_requests = self.request.POST['user_pull_requests']
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_artifacts=handle_artifacts)
UserModel().delete(
c.user,
handle_repos=handle_repos,
handle_repo_groups=handle_repo_groups,
handle_user_groups=handle_user_groups,
handle_pull_requests=handle_pull_requests,
handle_artifacts=handle_artifacts,
handle_new_owner=handle_user
)
audit_logger.store_web(
'user.delete', action_data={'old_data': old_values},
@ -449,11 +476,13 @@ class UsersView(UserAppView):
set_handle_flash_repos()
set_handle_flash_repo_groups()
set_handle_flash_user_groups()
set_handle_flash_pull_requests()
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:
UserOwnsUserGroupsException, UserOwnsPullRequestsException,
UserOwnsArtifactsException, DefaultUserException) as e:
h.flash(e, category='warning')
except Exception:
log.exception("Exception during deletion of user")
@ -502,6 +531,11 @@ class UsersView(UserAppView):
user_id = self.db_user_id
c.user = self.db_user
c.detach_user = User.get_first_super_admin()
detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
if detach_user_id:
c.detach_user = User.get_or_404(detach_user_id)
c.active = 'advanced'
c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
c.personal_repo_group_name = RepoGroupModel()\
@ -511,7 +545,6 @@ class UsersView(UserAppView):
(x.user for x in c.user.user_review_rules),
key=lambda u: u.username.lower())
c.first_admin = User.get_first_super_admin()
defaults = c.user.get_dict()
# Interim workaround if the user participated on any pull requests as a

View file

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

View file

@ -617,6 +617,7 @@ class User(Base, BaseModel):
user_gists = relationship('Gist', cascade='all')
# user pull requests
user_pull_requests = relationship('PullRequest', cascade='all')
# external identities
external_identities = relationship(
'ExternalIdentity',

View file

@ -43,7 +43,9 @@ from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.hooks_daemon import prepare_callback_daemon
from rhodecode.lib.markup_renderer import (
DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int
from rhodecode.lib.utils2 import (
safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
get_current_rhodecode_user)
from rhodecode.lib.vcs.backends.base import (
Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
TargetRefMissing, SourceRefMissing)
@ -1427,7 +1429,10 @@ class PullRequestModel(BaseModel):
email_kwargs=email_kwargs,
)
def delete(self, pull_request, user):
def delete(self, pull_request, user=None):
if not user:
user = getattr(get_current_rhodecode_user(), 'username', None)
pull_request = self.__get_pull_request(pull_request)
old_data = pull_request.get_api_data(with_merge_state=False)
self._cleanup_merge_workspace(pull_request)

View file

@ -37,17 +37,17 @@ from rhodecode.lib.utils2 import (
AttributeDict, str2bool)
from rhodecode.lib.exceptions import (
DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
UserOwnsUserGroupsException, NotAllowedToCreateUserError,
UserOwnsPullRequestsException, UserOwnsArtifactsException)
from rhodecode.lib.caching_query import FromCache
from rhodecode.model import BaseModel
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.db import (
_hash_key, true, false, or_, joinedload, User, UserToPerm,
UserEmailMap, UserIpMap, UserLog)
from rhodecode.model.meta import Session
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.repo_group import RepoGroupModel
log = logging.getLogger(__name__)
@ -261,7 +261,7 @@ class UserModel(BaseModel):
cur_user = getattr(get_current_rhodecode_user(), 'username', None)
from rhodecode.lib.auth import (
get_crypt_password, check_password, generate_auth_token)
get_crypt_password, check_password)
from rhodecode.lib.hooks_base import (
log_create_user, check_allowed_create_user)
@ -443,15 +443,16 @@ class UserModel(BaseModel):
log.error(traceback.format_exc())
raise
def _handle_user_repos(self, username, repositories, handle_mode=None):
_superadmin = self.cls.get_first_super_admin()
def _handle_user_repos(self, username, repositories, handle_user,
handle_mode=None):
left_overs = True
from rhodecode.model.repo import RepoModel
if handle_mode == 'detach':
for obj in repositories:
obj.user = _superadmin
obj.user = handle_user
# set description we know why we super admin now owns
# additional repositories that were orphaned !
obj.description += ' \n::detached repository from deleted user: %s' % (username,)
@ -465,16 +466,16 @@ class UserModel(BaseModel):
# if nothing is done we have left overs left
return left_overs
def _handle_user_repo_groups(self, username, repository_groups,
def _handle_user_repo_groups(self, username, repository_groups, handle_user,
handle_mode=None):
_superadmin = self.cls.get_first_super_admin()
left_overs = True
from rhodecode.model.repo_group import RepoGroupModel
if handle_mode == 'detach':
for r in repository_groups:
r.user = _superadmin
r.user = handle_user
# set description we know why we super admin now owns
# additional repositories that were orphaned !
r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
@ -489,8 +490,9 @@ class UserModel(BaseModel):
# if nothing is done we have left overs left
return left_overs
def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
_superadmin = self.cls.get_first_super_admin()
def _handle_user_user_groups(self, username, user_groups, handle_user,
handle_mode=None):
left_overs = True
from rhodecode.model.user_group import UserGroupModel
@ -499,8 +501,8 @@ class UserModel(BaseModel):
for r in user_groups:
for user_user_group_to_perm in r.user_user_group_to_perm:
if user_user_group_to_perm.user.username == username:
user_user_group_to_perm.user = _superadmin
r.user = _superadmin
user_user_group_to_perm.user = handle_user
r.user = handle_user
# set description we know why we super admin now owns
# additional repositories that were orphaned !
r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
@ -514,13 +516,37 @@ 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()
def _handle_user_pull_requests(self, username, pull_requests, handle_user,
handle_mode=None):
left_overs = True
from rhodecode.model.pull_request import PullRequestModel
if handle_mode == 'detach':
for pr in pull_requests:
pr.user_id = handle_user.user_id
# set description we know why we super admin now owns
# additional repositories that were orphaned !
pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
self.sa.add(pr)
left_overs = False
elif handle_mode == 'delete':
for pr in pull_requests:
PullRequestModel().delete(pr)
left_overs = False
# if nothing is done we have left overs left
return left_overs
def _handle_user_artifacts(self, username, artifacts, handle_user,
handle_mode=None):
left_overs = True
if handle_mode == 'detach':
for a in artifacts:
a.upload_user = _superadmin
a.upload_user = handle_user
# 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,)
@ -528,7 +554,8 @@ class UserModel(BaseModel):
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)
request = get_current_request()
storage = store_utils.get_file_storage(request.registry.settings)
for a in artifacts:
file_uid = a.file_uid
storage.delete(file_uid)
@ -540,11 +567,13 @@ class UserModel(BaseModel):
return left_overs
def delete(self, user, cur_user=None, handle_repos=None,
handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
handle_repo_groups=None, handle_user_groups=None,
handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
from rhodecode.lib.hooks_base import log_delete_user
if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), 'username', None)
user = self._get_user(user)
try:
@ -552,9 +581,11 @@ class UserModel(BaseModel):
raise DefaultUserException(
u"You can't remove this user since it's"
u" crucial for entire application")
handle_user = handle_new_owner or self.cls.get_first_super_admin()
log.debug('New detached objects owner %s', handle_user)
left_overs = self._handle_user_repos(
user.username, user.repositories, handle_repos)
user.username, user.repositories, handle_user, handle_repos)
if left_overs and user.repositories:
repos = [x.repo_name for x in user.repositories]
raise UserOwnsReposException(
@ -564,7 +595,7 @@ class UserModel(BaseModel):
'list_repos': ', '.join(repos)})
left_overs = self._handle_user_repo_groups(
user.username, user.repository_groups, handle_repo_groups)
user.username, user.repository_groups, handle_user, handle_repo_groups)
if left_overs and user.repository_groups:
repo_groups = [x.group_name for x in user.repository_groups]
raise UserOwnsRepoGroupsException(
@ -574,7 +605,7 @@ class UserModel(BaseModel):
'list_repo_groups': ', '.join(repo_groups)})
left_overs = self._handle_user_user_groups(
user.username, user.user_groups, handle_user_groups)
user.username, user.user_groups, handle_user, handle_user_groups)
if left_overs and user.user_groups:
user_groups = [x.users_group_name for x in user.user_groups]
raise UserOwnsUserGroupsException(
@ -582,8 +613,17 @@ 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_pull_requests(
user.username, user.user_pull_requests, handle_user, handle_pull_requests)
if left_overs and user.user_pull_requests:
pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
raise UserOwnsPullRequestsException(
u'user "%s" still owns %s pull requests and cannot be '
u'removed. Switch owners or remove those pull requests:%s'
% (user.username, len(pull_requests), ', '.join(pull_requests)))
left_overs = self._handle_user_artifacts(
user.username, user.artifacts, handle_artifacts)
user.username, user.artifacts, handle_user, handle_artifacts)
if left_overs and user.artifacts:
artifacts = [x.file_uid for x in user.artifacts]
raise UserOwnsArtifactsException(
@ -878,7 +918,7 @@ class UserModel(BaseModel):
end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
parsed_ip_range = []
for index in xrange(int(start_ip), int(end_ip) + 1):
for index in range(int(start_ip), int(end_ip) + 1):
new_ip = ipaddress.ip_address(index)
parsed_ip_range.append(str(new_ip))
ip_list.extend(parsed_ip_range)

View file

@ -147,6 +147,18 @@
</td>
</tr>
<tr>
<td>
${_ungettext('This user owns %s pull request.', 'This user owns %s pull requests.', len(c.user.user_pull_requests)) % len(c.user.user_pull_requests)}
</td>
<td>
<input type="radio" id="user_pull_requests_1" name="user_pull_requests" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_1">${_('Detach pull requests')}</label>
</td>
<td>
<input type="radio" id="user_pull_requests_2" name="user_pull_requests" value="delete" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_2">${_('Delete pull requests')}</label>
</td>
</tr>
<tr>
<td>
${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
@ -166,7 +178,8 @@
% endif
<span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span>
<div class="pull-right">${base.gravatar_with_user(c.first_admin.email, 16)}</div>
<div class="pull-right">${base.gravatar_with_user(c.detach_user.email, 16, tooltip=True)}</div>
<input type="hidden" name="detach_user_id" value="${c.detach_user.user_id}">
</div>
<div style="clear: both">
@ -186,11 +199,11 @@
<div style="margin: 0 0 20px 0" class="fake-space"></div>
<div class="field">
<button class="btn btn-small btn-danger" type="submit"
onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
${"disabled" if not c.can_delete_user else ""}>
${_('Delete this user')}
</button>
<input class="btn btn-small btn-danger" id="remove_user" name="remove_user"
onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Confirm Delete'), '${c.user.username}')"
${("disabled=1" if not c.can_delete_user else "")}
type="submit" value="${_('Delete this user')}"
>
</div>
${h.end_form()}