##// END OF EJS Templates
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
marcink -
r4011:e2f9b772 default
parent child Browse files
Show More
@@ -517,7 +517,7 b' class TestAdminUsersView(TestController)'
517 route_path('user_delete', user_id=new_user.user_id),
517 route_path('user_delete', user_id=new_user.user_id),
518 params={'csrf_token': self.csrf_token})
518 params={'csrf_token': self.csrf_token})
519
519
520 assert_session_flash(response, 'Successfully deleted user')
520 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
521
521
522 def test_delete_owner_of_repository(self, request, user_util):
522 def test_delete_owner_of_repository(self, request, user_util):
523 self.log_user()
523 self.log_user()
@@ -376,59 +376,69 b' class UsersView(UserAppView):'
376 _repos = c.user.repositories
376 _repos = c.user.repositories
377 _repo_groups = c.user.repository_groups
377 _repo_groups = c.user.repository_groups
378 _user_groups = c.user.user_groups
378 _user_groups = c.user.user_groups
379 _artifacts = c.user.artifacts
379
380
380 handle_repos = None
381 handle_repos = None
381 handle_repo_groups = None
382 handle_repo_groups = None
382 handle_user_groups = None
383 handle_user_groups = None
383 # dummy call for flash of handle
384 handle_artifacts = None
384 set_handle_flash_repos = lambda: None
385
385 set_handle_flash_repo_groups = lambda: None
386 # calls for flash of handle based on handle case detach or delete
386 set_handle_flash_user_groups = lambda: None
387 def set_handle_flash_repos():
388 handle = handle_repos
389 if handle == 'detach':
390 h.flash(_('Detached %s repositories') % len(_repos),
391 category='success')
392 elif handle == 'delete':
393 h.flash(_('Deleted %s repositories') % len(_repos),
394 category='success')
395
396 def set_handle_flash_repo_groups():
397 handle = handle_repo_groups
398 if handle == 'detach':
399 h.flash(_('Detached %s repository groups') % len(_repo_groups),
400 category='success')
401 elif handle == 'delete':
402 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
403 category='success')
404
405 def set_handle_flash_user_groups():
406 handle = handle_user_groups
407 if handle == 'detach':
408 h.flash(_('Detached %s user groups') % len(_user_groups),
409 category='success')
410 elif handle == 'delete':
411 h.flash(_('Deleted %s user groups') % len(_user_groups),
412 category='success')
413
414 def set_handle_flash_artifacts():
415 handle = handle_artifacts
416 if handle == 'detach':
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 category='success')
419 elif handle == 'delete':
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 category='success')
387
422
388 if _repos and self.request.POST.get('user_repos'):
423 if _repos and self.request.POST.get('user_repos'):
389 do = self.request.POST['user_repos']
424 handle_repos = self.request.POST['user_repos']
390 if do == 'detach':
391 handle_repos = 'detach'
392 set_handle_flash_repos = lambda: h.flash(
393 _('Detached %s repositories') % len(_repos),
394 category='success')
395 elif do == 'delete':
396 handle_repos = 'delete'
397 set_handle_flash_repos = lambda: h.flash(
398 _('Deleted %s repositories') % len(_repos),
399 category='success')
400
425
401 if _repo_groups and self.request.POST.get('user_repo_groups'):
426 if _repo_groups and self.request.POST.get('user_repo_groups'):
402 do = self.request.POST['user_repo_groups']
427 handle_repo_groups = self.request.POST['user_repo_groups']
403 if do == 'detach':
404 handle_repo_groups = 'detach'
405 set_handle_flash_repo_groups = lambda: h.flash(
406 _('Detached %s repository groups') % len(_repo_groups),
407 category='success')
408 elif do == 'delete':
409 handle_repo_groups = 'delete'
410 set_handle_flash_repo_groups = lambda: h.flash(
411 _('Deleted %s repository groups') % len(_repo_groups),
412 category='success')
413
428
414 if _user_groups and self.request.POST.get('user_user_groups'):
429 if _user_groups and self.request.POST.get('user_user_groups'):
415 do = self.request.POST['user_user_groups']
430 handle_user_groups = self.request.POST['user_user_groups']
416 if do == 'detach':
431
417 handle_user_groups = 'detach'
432 if _artifacts and self.request.POST.get('user_artifacts'):
418 set_handle_flash_user_groups = lambda: h.flash(
433 handle_artifacts = self.request.POST['user_artifacts']
419 _('Detached %s user groups') % len(_user_groups),
420 category='success')
421 elif do == 'delete':
422 handle_user_groups = 'delete'
423 set_handle_flash_user_groups = lambda: h.flash(
424 _('Deleted %s user groups') % len(_user_groups),
425 category='success')
426
434
427 old_values = c.user.get_api_data()
435 old_values = c.user.get_api_data()
436
428 try:
437 try:
429 UserModel().delete(c.user, handle_repos=handle_repos,
438 UserModel().delete(c.user, handle_repos=handle_repos,
430 handle_repo_groups=handle_repo_groups,
439 handle_repo_groups=handle_repo_groups,
431 handle_user_groups=handle_user_groups)
440 handle_user_groups=handle_user_groups,
441 handle_artifacts=handle_artifacts)
432
442
433 audit_logger.store_web(
443 audit_logger.store_web(
434 'user.delete', action_data={'old_data': old_values},
444 'user.delete', action_data={'old_data': old_values},
@@ -438,7 +448,9 b' class UsersView(UserAppView):'
438 set_handle_flash_repos()
448 set_handle_flash_repos()
439 set_handle_flash_repo_groups()
449 set_handle_flash_repo_groups()
440 set_handle_flash_user_groups()
450 set_handle_flash_user_groups()
441 h.flash(_('Successfully deleted user'), category='success')
451 set_handle_flash_artifacts()
452 username = h.escape(old_values['username'])
453 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
442 except (UserOwnsReposException, UserOwnsRepoGroupsException,
454 except (UserOwnsReposException, UserOwnsRepoGroupsException,
443 UserOwnsUserGroupsException, DefaultUserException) as e:
455 UserOwnsUserGroupsException, DefaultUserException) as e:
444 h.flash(e, category='warning')
456 h.flash(e, category='warning')
@@ -58,6 +58,10 b' class UserOwnsUserGroupsException(Except'
58 pass
58 pass
59
59
60
60
61 class UserOwnsArtifactsException(Exception):
62 pass
63
64
61 class UserGroupAssignedException(Exception):
65 class UserGroupAssignedException(Exception):
62 pass
66 pass
63
67
@@ -617,13 +617,19 b' class User(Base, BaseModel):'
617 # user pull requests
617 # user pull requests
618 user_pull_requests = relationship('PullRequest', cascade='all')
618 user_pull_requests = relationship('PullRequest', cascade='all')
619 # external identities
619 # external identities
620 extenal_identities = relationship(
620 external_identities = relationship(
621 'ExternalIdentity',
621 'ExternalIdentity',
622 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
622 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
623 cascade='all')
623 cascade='all')
624 # review rules
624 # review rules
625 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
625 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
626
626
627 # artifacts owned
628 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
629
630 # no cascade, set NULL
631 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
632
627 def __unicode__(self):
633 def __unicode__(self):
628 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
634 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
629 self.user_id, self.username)
635 self.user_id, self.username)
@@ -1704,7 +1710,8 b' class Repository(Base, BaseModel):'
1704
1710
1705 scoped_tokens = relationship('UserApiKeys', cascade="all")
1711 scoped_tokens = relationship('UserApiKeys', cascade="all")
1706
1712
1707 artifacts = relationship('FileStore', cascade="all")
1713 # no cascade, set NULL
1714 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1708
1715
1709 def __unicode__(self):
1716 def __unicode__(self):
1710 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1717 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
@@ -2579,6 +2586,9 b' class RepoGroup(Base, BaseModel):'
2579 user = relationship('User')
2586 user = relationship('User')
2580 integrations = relationship('Integration', cascade="all, delete-orphan")
2587 integrations = relationship('Integration', cascade="all, delete-orphan")
2581
2588
2589 # no cascade, set NULL
2590 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2591
2582 def __init__(self, group_name='', parent_group=None):
2592 def __init__(self, group_name='', parent_group=None):
2583 self.group_name = group_name
2593 self.group_name = group_name
2584 self.parent_group = parent_group
2594 self.parent_group = parent_group
@@ -3870,6 +3880,7 b' class _SetState(object):'
3870 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3880 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3871 raise
3881 raise
3872
3882
3883
3873 class _PullRequestBase(BaseModel):
3884 class _PullRequestBase(BaseModel):
3874 """
3885 """
3875 Common attributes of pull request and version entries.
3886 Common attributes of pull request and version entries.
@@ -4148,14 +4159,10 b' class PullRequest(Base, _PullRequestBase'
4148 else:
4159 else:
4149 return '<DB:PullRequest at %#x>' % id(self)
4160 return '<DB:PullRequest at %#x>' % id(self)
4150
4161
4151 reviewers = relationship('PullRequestReviewers',
4162 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4152 cascade="all, delete-orphan")
4163 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4153 statuses = relationship('ChangesetStatus',
4164 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4154 cascade="all, delete-orphan")
4165 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4155 comments = relationship('ChangesetComment',
4156 cascade="all, delete-orphan")
4157 versions = relationship('PullRequestVersion',
4158 cascade="all, delete-orphan",
4159 lazy='dynamic')
4166 lazy='dynamic')
4160
4167
4161 @classmethod
4168 @classmethod
@@ -37,7 +37,7 b' from rhodecode.lib.utils2 import ('
37 AttributeDict, str2bool)
37 AttributeDict, str2bool)
38 from rhodecode.lib.exceptions import (
38 from rhodecode.lib.exceptions import (
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
41 from rhodecode.lib.caching_query import FromCache
41 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.model import BaseModel
42 from rhodecode.model import BaseModel
43 from rhodecode.model.auth_token import AuthTokenModel
43 from rhodecode.model.auth_token import AuthTokenModel
@@ -505,8 +505,33 b' class UserModel(BaseModel):'
505 # if nothing is done we have left overs left
505 # if nothing is done we have left overs left
506 return left_overs
506 return left_overs
507
507
508 def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
509 _superadmin = self.cls.get_first_super_admin()
510 left_overs = True
511
512 if handle_mode == 'detach':
513 for a in artifacts:
514 a.upload_user = _superadmin
515 # set description we know why we super admin now owns
516 # additional artifacts that were orphaned !
517 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
518 self.sa.add(a)
519 left_overs = False
520 elif handle_mode == 'delete':
521 from rhodecode.apps.file_store import utils as store_utils
522 storage = store_utils.get_file_storage(self.request.registry.settings)
523 for a in artifacts:
524 file_uid = a.file_uid
525 storage.delete(file_uid)
526 self.sa.delete(a)
527
528 left_overs = False
529
530 # if nothing is done we have left overs left
531 return left_overs
532
508 def delete(self, user, cur_user=None, handle_repos=None,
533 def delete(self, user, cur_user=None, handle_repos=None,
509 handle_repo_groups=None, handle_user_groups=None):
534 handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
510 from rhodecode.lib.hooks_base import log_delete_user
535 from rhodecode.lib.hooks_base import log_delete_user
511
536
512 if not cur_user:
537 if not cur_user:
@@ -548,6 +573,15 b' class UserModel(BaseModel):'
548 u'removed. Switch owners or remove those user groups:%s'
573 u'removed. Switch owners or remove those user groups:%s'
549 % (user.username, len(user_groups), ', '.join(user_groups)))
574 % (user.username, len(user_groups), ', '.join(user_groups)))
550
575
576 left_overs = self._handle_user_artifacts(
577 user.username, user.artifacts, handle_artifacts)
578 if left_overs and user.artifacts:
579 artifacts = [x.file_uid for x in user.artifacts]
580 raise UserOwnsArtifactsException(
581 u'user "%s" still owns %s artifacts and cannot be '
582 u'removed. Switch owners or remove those artifacts:%s'
583 % (user.username, len(artifacts), ', '.join(artifacts)))
584
551 user_data = user.get_dict() # fetch user data before expire
585 user_data = user.get_dict() # fetch user data before expire
552
586
553 # we might change the user data with detach/delete, make sure
587 # we might change the user data with detach/delete, make sure
@@ -13,6 +13,8 b''
13 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
13 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
14 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
14 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
15
15
16 (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]),
17
16 (_('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]),
18 (_('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]),
17 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
19 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
18
20
@@ -23,10 +25,19 b''
23
25
24 <div class="panel panel-default">
26 <div class="panel panel-default">
25 <div class="panel-heading">
27 <div class="panel-heading">
26 <h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
28 <h3 class="panel-title">${_('User: {}').format(c.user.username)}</h3>
27 </div>
29 </div>
28 <div class="panel-body">
30 <div class="panel-body">
29 ${base.dt_info_panel(elems)}
31 <table class="rctable">
32 <tr>
33 <th>Name</th>
34 <th>Value</th>
35 <th>Action</th>
36 </tr>
37 % for elem in elems:
38 ${base.tr_info_entry(elem)}
39 % endfor
40 </table>
30 </div>
41 </div>
31 </div>
42 </div>
32
43
@@ -132,6 +143,19 b''
132 <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>
143 <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>
133 </td>
144 </td>
134 </tr>
145 </tr>
146
147 <tr>
148 <td>
149 ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
150 </td>
151 <td>
152 <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>
153 </td>
154 <td>
155 <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>
156 </td>
157 </tr>
158
135 </table>
159 </table>
136 <div style="margin: 0 0 20px 0" class="fake-space"></div>
160 <div style="margin: 0 0 20px 0" class="fake-space"></div>
137 <div class="pull-left">
161 <div class="pull-left">
@@ -164,6 +164,36 b''
164 </dl>
164 </dl>
165 </%def>
165 </%def>
166
166
167 <%def name="tr_info_entry(element)">
168 <% key, val, title, show_items = element %>
169
170 <tr>
171 <td style="vertical-align: top">${key}</td>
172 <td title="${h.tooltip(title)}">
173 %if callable(val):
174 ## allow lazy evaluation of elements
175 ${val()}
176 %else:
177 ${val}
178 %endif
179 %if show_items:
180 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
181 % for item in show_items:
182 <dt></dt>
183 <dd>${item}</dd>
184 % endfor
185 </div>
186 %endif
187 </td>
188 <td style="vertical-align: top">
189 %if show_items:
190 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
191 %endif
192 </td>
193 </tr>
194
195 </%def>
196
167 <%def name="gravatar(email, size=16)">
197 <%def name="gravatar(email, size=16)">
168 <%
198 <%
169 if (size > 16):
199 if (size > 16):
General Comments 0
You need to be logged in to leave comments. Login now