"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license. """ """ Actions for tasks and annotations provided by data manager. All actions are stored in settings.DATA_MANAGER_ACTIONS dict. Data manager uses settings.DATA_MANAGER_ACTIONS to know the list of available actions, they are called by entry_points from settings.DATA_MANAGER_ACTIONS dict items. """ import copy import logging import os import traceback as tb from importlib import import_module from typing import Callable, Optional, TypedDict, Union from core.feature_flags import flag_set from core.utils.common import load_func from data_manager.functions import DataManagerException from django.conf import settings from rest_framework.exceptions import PermissionDenied logger = logging.getLogger(__name__) class DataManagerAction(TypedDict): entry_point: Callable permission: Union[str, list[str]] title: str order: int experimental: Optional[bool] dialog: dict hidden: Optional[bool] disabled: Optional[Callable] disabled_reason: Optional[str] def check_action_permission(user, action, project): """Actions must have permissions, if only one is in the user role then the action is allowed""" if 'permission' not in action: logger.error('Action must have "permission" field: %s', str(action)) return False permissions = action['permission'] if not isinstance(permissions, list): permissions = [permissions] for permission in permissions: if not user.has_perm(permission): return False return True def get_all_actions(user, project): """Return dict with registered actions :param user: list with user permissions :param project: current project """ # copy and sort by order key actions = list(settings.DATA_MANAGER_ACTIONS.values()) actions = copy.deepcopy(actions) actions: list[DataManagerAction] = sorted(actions, key=lambda x: x['order']) check_permission = load_func(settings.DATA_MANAGER_CHECK_ACTION_PERMISSION) actions = [ {key: action[key] for key in action if key != 'entry_point'} for action in actions if not action.get('hidden', False) and check_permission(user, action, project) ] # remove experimental features if they are disabled if not ( flag_set('ff_back_experimental_features', user=project.organization.created_by) or settings.EXPERIMENTAL_FEATURES ): actions = [action for action in actions if not action.get('experimental', False)] for action in actions: # remove form if generator is defined # will be loaded on demand in /api/actions//form form_generator = action.get('dialog', {}).get('form') if callable(form_generator): action['dialog']['form'] = None disabled_generator = action.get('disabled') if callable(disabled_generator): action['disabled'] = disabled_generator(user, project) return actions def register_action(entry_point, title, order, **kwargs): """Register action in global _action instance, action_id will be automatically extracted from entry_point function name """ action_id = entry_point.__name__ if action_id in settings.DATA_MANAGER_ACTIONS: logger.debug('Action with id "' + action_id + '" already exists, rewriting registration') settings.DATA_MANAGER_ACTIONS[action_id] = { 'id': action_id, 'title': title, 'order': order, 'entry_point': entry_point, **kwargs, } def register_actions_from_dir(base_module, action_dir): """Find all python files nearby this file and try to load 'actions' from them""" for path in os.listdir(action_dir): # skip non module files if '__init__' in path or '__pycache' in path or path.startswith('.'): continue name = path[0 : path.find('.py')] # get only module name to read *.py and *.pyc try: module = import_module(f'{base_module}.{name}') if not hasattr(module, 'actions'): continue module_actions = module.actions except ModuleNotFoundError as e: logger.info(e) continue for action in module_actions: register_action(**action) logger.debug('Action registered: ' + str(action['entry_point'].__name__)) def perform_action(action_id, project, queryset, user, **kwargs): """Perform action using entry point from actions""" if action_id not in settings.DATA_MANAGER_ACTIONS: raise DataManagerException("Can't find '" + action_id + "' in registered actions") action = settings.DATA_MANAGER_ACTIONS[action_id] check_permission = load_func(settings.DATA_MANAGER_CHECK_ACTION_PERMISSION) # check user permissions for this action if not check_permission(user, action, project): raise PermissionDenied(f'Action is not allowed for the current user: {action["id"]}') try: result = action['entry_point'](project, queryset, **kwargs) except Exception as e: text = 'Error while perform action: ' + action_id + '\n' + tb.format_exc() logger.error(text, extra={'sentry_skip': True}) raise e return result def get_action_form(action_id, project, user): if action_id not in settings.DATA_MANAGER_ACTIONS: raise DataManagerException("Can't find '" + action_id + "' in registered actions") action = settings.DATA_MANAGER_ACTIONS[action_id] check_permission = load_func(settings.DATA_MANAGER_CHECK_ACTION_PERMISSION) if not check_permission(user, action, project): raise PermissionDenied(f'Action is not allowed for the current user: {action["id"]}') form = action.get('dialog', {}).get('form') if callable(form): return form(user, project) return form or [] register_actions_from_dir('data_manager.actions', os.path.dirname(__file__))