Skip to content

Commit

Permalink
add removeroles management command (#1391)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Jan 10, 2025
1 parent 09a87f0 commit c99b2a6
Show file tree
Hide file tree
Showing 8 changed files with 548 additions and 74 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Added
- Django check for unique app setting names within each plugin (#1456)
- App setting ``user_modifiable`` validation (#1536)
- ``AppSettingAPI.get_all_by_scope()`` helper (#1534)
- ``removeroles`` management command (#1391)

Changed
-------
Expand All @@ -30,6 +31,8 @@ Changed
- Deprecate declaring app setting definitions as dict (#1456)
- Allow ``scope=None`` in ``AppSettingAPI.get_definitions()`` (#1535)
- Deprecate ``AppSettingAPI.get_all()`` (#1534)
- Allow no role for old owner in ``RoleAssignmentOwnerTransferMixin`` (#836, #1391)
- Allow no role for old owner in ``perform_owner_transfer()`` (#836, #1391)

Removed
-------
Expand Down
13 changes: 13 additions & 0 deletions docs/source/app_projectroles_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,19 @@ project permissions, or by a site admin using the ``batchupdateroles``
management command. The latter supports multiple projects in one batch. It is
also able to send invites to users who have not yet signed up on the site.

Remove All Roles from User
--------------------------

To easily remove all roles from a user, use the ``removeroles`` management
command. For owner roles, you can supply the user name of a user for whom to
transfer those roles. If no owner is supplied, each ownership will be
transferred to the parent category owner. Example:

.. code-block:: console
$ ./manage.py removeroles --user alice --owner bob
User Status Checking
--------------------

Expand Down
9 changes: 9 additions & 0 deletions docs/source/major_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ v1.1.0 (WIP)
Release Highlights
==================

- Add removeroles management command
- Add app setting type constants
- Add app setting definition as objects
- Update app settings API
Expand All @@ -34,6 +35,14 @@ instead of dict, the return data of ``AppSettingAPI.get_definition()`` and
class. The return data is the same even if definitions have been provided in the
deprecated dictionary format.

Old Owner Role in Project Modify API Ownership Transfer
-------------------------------------------------------

In ``ProjectModifyPluginMixin.perform_role_modify()``, the ``old_owner_role``
argument may be ``None``. This is used in cases where project role for the
previous owner is removed. Implementations of ``perform_role_modify()`` must be
changed accordingly. The same also applies to ``revert_role_modify()``.

Deprecated Features
-------------------

Expand Down
178 changes: 178 additions & 0 deletions projectroles/management/commands/removeroles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
Removeroles management command for removing all roles from a user.
"""

import sys

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.db import transaction

from projectroles.management.logging import ManagementCommandLogger
from projectroles.models import RoleAssignment, SODAR_CONSTANTS
from projectroles.views import (
RoleAssignmentOwnerTransferMixin,
RoleAssignmentDeleteMixin,
)

logger = ManagementCommandLogger(__name__)
User = get_user_model()


# SODAR constants
PROJECT_ROLE_OWNER = SODAR_CONSTANTS['PROJECT_ROLE_OWNER']

# Local constants
USER_NOT_FOUND_MSG = 'User not found with username: {}'


class Command(
RoleAssignmentOwnerTransferMixin, RoleAssignmentDeleteMixin, BaseCommand
):
help = (
'Remove all roles from a user. Replace owner roles with given user or '
'parent owner.'
)

@classmethod
def _get_parent_owner(cls, project, prev_owner):
"""Return assignment for first parent owner who is not previous owner"""
if not project.parent:
return None
parent_owner_as = project.parent.get_owner()
if parent_owner_as.user != prev_owner:
return parent_owner_as.user
return cls._get_parent_owner(project.parent, prev_owner)

def _reassign_owner(self, project, user, owner, role_as, p_title):
"""Reassign owner role"""
# Fail top category if no new owner is specified
if not owner and not project.parent:
logger.warning(
'Failed to transfer ownership for top level {} {}: no '
'new owner provided'.format(project.type.lower(), p_title)
)
return False
# Get parent owner if not set
if not owner:
owner = self._get_parent_owner(project, user)
# Fail if alternate parent owner is not found
if not owner:
logger.warning(
'Failed to transfer ownership in {}: no parent owner '
'found'.format(p_title)
)
return False
try:
with transaction.atomic():
self.transfer_owner(
project=project,
new_owner=owner,
old_owner_as=role_as,
old_owner_role=None,
notify_old=False,
)
logger.info(
'Transferred ownership in {} to {}'.format(
p_title, owner.username
)
)
return True
except Exception as ex:
logger.error(
'Failed to transfer ownership in {} to {}: {}'.format(
p_title, owner.username, ex
)
)
return False

def _remove_role(self, role_as, p_title):
"""Remove non-owner role"""
r_name = role_as.role.name
try:
with transaction.atomic():
self.delete_assignment(role_as, None, False)
logger.info('Deleted role "{}" from {}'.format(r_name, p_title))
return True
except Exception as ex:
logger.error(
'Failed to delete assignment "{}" from {}: '
'{}'.format(r_name, p_title, ex)
)
return False

def add_arguments(self, parser):
parser.add_argument(
'-u',
'--user',
dest='user',
required=True,
help='User name of user whose roles will be removed',
)
parser.add_argument(
'-o',
'--owner',
dest='owner',
required=False,
help='Set owner role for user by given user name if set, otherwise '
'set to parent owner',
)

def handle(self, *args, **options):
if options['user'] == options.get('owner'):
logger.error(
'Same username given for both user and new owner: {}'.format(
options['user']
)
)
sys.exit(1)
try:
user = User.objects.get(username=options['user'])
except User.DoesNotExist:
logger.error(USER_NOT_FOUND_MSG.format(options['user']))
sys.exit(1)
owner_name = options.get('owner')
owner = None
if owner_name:
try:
owner = User.objects.get(username=owner_name)
except User.DoesNotExist:
logger.error(USER_NOT_FOUND_MSG.format(owner_name))
sys.exit(1)

logger.info('Removing roles from user "{}"..'.format(user.username))
if owner:
logger.info(
'New owner for replacing owner roles: {}'.format(owner.username)
)
role_count = 0
fail_count = 0
roles = RoleAssignment.objects.filter(user=user).order_by(
'project__full_title'
)
if roles.count() == 0:
logger.info('No roles found')
return

for role_as in roles:
project = role_as.project
p_title = project.get_log_title()
if project.is_remote(): # Skip remote projects
logger.debug('Skipping remote project: {}'.format(p_title))
continue
# Owner role reassignment
if role_as.role.name == PROJECT_ROLE_OWNER:
update_ok = self._reassign_owner(
project, user, owner, role_as, p_title
)
else: # Non-owner role removal
update_ok = self._remove_role(role_as, p_title)
if update_ok is True:
role_count += 1
else:
fail_count += 1
logger.info(
'Removed roles from user "{}" ({} OK, {} failed)'.format(
user.username, role_count, fail_count
)
)
8 changes: 4 additions & 4 deletions projectroles/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,29 +140,29 @@ def revert_role_delete(self, role_as, request=None):
pass

def perform_owner_transfer(
self, project, new_owner, old_owner, old_owner_role, request=None
self, project, new_owner, old_owner, old_owner_role=None, request=None
):
"""
Perform additional actions to finalize project ownership transfer.
:param project: Project object
:param new_owner: SODARUser object for new owner
:param old_owner: SODARUser object for previous owner
:param old_owner_role: Role object for new role of previous owner
:param old_owner_role: Role object for new role of old owner or None
:param request: Request object or None
"""
pass

def revert_owner_transfer(
self, project, new_owner, old_owner, old_owner_role, request=None
self, project, new_owner, old_owner, old_owner_role=None, request=None
):
"""
Revert project ownership transfer if errors have occurred in other apps.
:param project: Project object
:param new_owner: SODARUser object for new owner
:param old_owner: SODARUser object for previous owner
:param old_owner_role: Role object for new role of previous owner
:param old_owner_role: Role object for new role of old owner or None
:param request: Request object or None
"""
pass
Expand Down
Loading

0 comments on commit c99b2a6

Please sign in to comment.