# HG changeset patch # User Daniel Dourvaris # Date 2020-05-20 10:33:43 # Node ID 2d86851ba7ba82e50f63aac8c0c71bb211857c3e # Parent 365337eba172d73f4984d13b662552b066761d8a 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. diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -39,7 +39,8 @@ from rhodecode.model.db import true, Use 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 diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -58,6 +58,10 @@ class UserOwnsUserGroupsException(Except pass +class UserOwnsPullRequestsException(Exception): + pass + + class UserOwnsArtifactsException(Exception): pass diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -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', diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -43,7 +43,9 @@ from rhodecode.lib.compat import Ordered 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) diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -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) diff --git a/rhodecode/templates/admin/users/user_edit_advanced.mako b/rhodecode/templates/admin/users/user_edit_advanced.mako --- a/rhodecode/templates/admin/users/user_edit_advanced.mako +++ b/rhodecode/templates/admin/users/user_edit_advanced.mako @@ -149,6 +149,18 @@ + ${_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)} + + + + + + + + + + + ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)} @@ -166,7 +178,8 @@ % endif ${_('New owner for detached objects')}: -
${base.gravatar_with_user(c.first_admin.email, 16)}
+
${base.gravatar_with_user(c.detach_user.email, 16, tooltip=True)}
+
@@ -186,11 +199,11 @@
- +
${h.end_form()}