diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 4adaa1942..05297fbbd 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -28,7 +28,7 @@ from common.utils.common import get_file_by_arch from orgs.mixins.api import RootOrgViewMixin from common.http import is_true from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user -from perms.models.asset_permission import Action +from perms.models.base import Action from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, @@ -231,7 +231,7 @@ class SecretDetailMixin: @staticmethod def _get_application_secret_detail(application): - from perms.models import Action + from perms.models.base import Action gateway = None if not application.category_remote_app: @@ -391,10 +391,10 @@ class UserConnectionTokenViewSet( asset = get_object_or_404(Asset, id=value.get('asset')) if not asset.is_active: raise serializers.ValidationError("Asset disabled") - has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect') + has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user) else: app = get_object_or_404(Application, id=value.get('application')) - has_perm, expired_at = app_validate_permission(user, app, system_user) + has_perm, actions, expired_at = app_validate_permission(user, app, system_user) if not has_perm: raise serializers.ValidationError('Permission expired or invalid') diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 44fea3242..3c02dea99 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -9,7 +9,7 @@ from assets.models import Asset, SystemUser, Gateway from applications.models import Application from users.serializers import UserProfileSerializer from assets.serializers import ProtocolsField -from perms.serializers.asset.permission import ActionsField +from perms.serializers.base import ActionsField from .models import AccessKey __all__ = [ diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py index 2a64d122d..560fcb519 100644 --- a/apps/perms/api/application/user_permission/common.py +++ b/apps/perms/api/application/user_permission/common.py @@ -64,13 +64,22 @@ class ValidateUserApplicationPermissionApi(APIView): application_id = request.query_params.get('application_id', '') system_user_id = request.query_params.get('system_user_id', '') + data = { + 'has_permission': False, + 'expire_at': int(time.time()), + 'actions': [] + } if not all((user_id, application_id, system_user_id)): - return Response({'has_permission': False, 'expire_at': int(time.time())}) + return Response(data) user = User.objects.get(id=user_id) application = Application.objects.get(id=application_id) system_user = SystemUser.objects.get(id=system_user_id) - - has_permission, expire_at = validate_permission(user, application, system_user) - status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN - return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) + has_perm, actions, expire_at = validate_permission(user, application, system_user) + status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN + data = { + 'has_permission': has_perm, + 'expire_at': int(expire_at), + 'actions': actions + } + return Response(data, status=status_code) diff --git a/apps/perms/api/asset/user_permission/common.py b/apps/perms/api/asset/user_permission/common.py index 1d09274c8..e874d591f 100644 --- a/apps/perms/api/asset/user_permission/common.py +++ b/apps/perms/api/asset/user_permission/common.py @@ -72,16 +72,27 @@ class ValidateUserAssetPermissionApi(APIView): system_id = request.query_params.get('system_user_id', '') action_name = request.query_params.get('action_name', '') + data = { + 'has_permission': False, + 'expire_at': int(time.time()), + 'actions': [] + } + if not all((user_id, asset_id, system_id, action_name)): - return Response({'has_permission': False, 'expire_at': int(time.time())}) + return Response(data) user = User.objects.get(id=user_id) asset = Asset.objects.valid().get(id=asset_id) system_user = SystemUser.objects.get(id=system_id) - has_permission, expire_at = validate_permission(user, asset, system_user, action_name) - status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN - return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) + has_perm, actions, expire_at = validate_permission(user, asset, system_user, action_name) + status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN + data = { + 'has_permission': has_perm, + 'actions': actions, + 'expire_at': int(expire_at) + } + return Response(data, status=status_code) # TODO 删除 diff --git a/apps/perms/migrations/0011_auto_20200721_1739.py b/apps/perms/migrations/0011_auto_20200721_1739.py index 7e6b37188..352bd6b79 100644 --- a/apps/perms/migrations/0011_auto_20200721_1739.py +++ b/apps/perms/migrations/0011_auto_20200721_1739.py @@ -3,7 +3,7 @@ from django.db import migrations, models from django.db.models import F -from ..models.asset_permission import Action +from ..models.base import Action def migrate_asset_permission(apps, schema_editor): diff --git a/apps/perms/migrations/0022_applicationpermission_actions.py b/apps/perms/migrations/0022_applicationpermission_actions.py new file mode 100644 index 000000000..3026b1326 --- /dev/null +++ b/apps/perms/migrations/0022_applicationpermission_actions.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.13 on 2021-12-20 06:55 + +from django.db import migrations, models + +ACTION_CONNECT = 1 + + +def migrate_app_perms_actions(apps, schema_editor): + perm_model = apps.get_model("perms", "ApplicationPermission") + perm_model.objects.all().update(actions=ACTION_CONNECT) + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0021_auto_20211105_1605'), + ] + + operations = [ + migrations.AddField( + model_name='applicationpermission', + name='actions', + field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'), + ), + migrations.RunPython(migrate_app_perms_actions) + ] diff --git a/apps/perms/models/__init__.py b/apps/perms/models/__init__.py index 31a1344eb..e2ac0c1d6 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .application_permission import * +from .base import * diff --git a/apps/perms/models/application_permission.py b/apps/perms/models/application_permission.py index 40ac61ed8..bec5f3da5 100644 --- a/apps/perms/models/application_permission.py +++ b/apps/perms/models/application_permission.py @@ -6,7 +6,7 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty -from .base import BasePermission +from .base import BasePermission, Action from users.models import User from applications.const import AppCategory, AppType @@ -72,3 +72,31 @@ class ApplicationPermission(BasePermission): Q(id__in=user_ids) | Q(groups__id__in=user_group_ids) ) return users + + @classmethod + def get_include_actions_choices(cls, category=None): + actions = {Action.ALL, Action.CONNECT} + if category == AppCategory.db: + _actions = [Action.UPLOAD, Action.DOWNLOAD] + elif category == AppCategory.remote_app: + _actions = [ + Action.UPLOAD, Action.DOWNLOAD, + Action.CLIPBOARD_COPY, Action.CLIPBOARD_PASTE + ] + else: + _actions = [] + actions.update(_actions) + + if (Action.UPLOAD in actions) or (Action.DOWNLOAD in actions): + actions.update([Action.UPDOWNLOAD]) + if (Action.CLIPBOARD_COPY in actions) or (Action.CLIPBOARD_PASTE in actions): + actions.update([Action.CLIPBOARD_COPY_PASTE]) + + choices = [Action.NAME_MAP[action] for action in actions] + return choices + + @classmethod + def get_exclude_actions_choices(cls, category=None): + include_choices = cls.get_include_actions_choices(category) + exclude_choices = set(Action.NAME_MAP.values()) - set(include_choices) + return exclude_choices diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 78e6b9b5b..7947449e7 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,5 +1,4 @@ import logging -from functools import reduce from django.utils.translation import ugettext_lazy as _ from django.db.models import F @@ -14,92 +13,17 @@ from .base import BasePermission __all__ = [ - 'AssetPermission', 'Action', 'PermNode', 'UserAssetGrantedTreeNodeRelation', + 'AssetPermission', 'PermNode', 'UserAssetGrantedTreeNodeRelation', ] # 使用场景 logger = logging.getLogger(__name__) -class Action: - NONE = 0 - - CONNECT = 0b1 - UPLOAD = 0b1 << 1 - DOWNLOAD = 0b1 << 2 - CLIPBOARD_COPY = 0b1 << 3 - CLIPBOARD_PASTE = 0b1 << 4 - ALL = 0xff - UPDOWNLOAD = UPLOAD | DOWNLOAD - CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE - - DB_CHOICES = ( - (ALL, _('All')), - (CONNECT, _('Connect')), - (UPLOAD, _('Upload file')), - (DOWNLOAD, _('Download file')), - (UPDOWNLOAD, _("Upload download")), - (CLIPBOARD_COPY, _('Clipboard copy')), - (CLIPBOARD_PASTE, _('Clipboard paste')), - (CLIPBOARD_COPY_PASTE, _('Clipboard copy paste')) - ) - - NAME_MAP = { - ALL: "all", - CONNECT: "connect", - UPLOAD: "upload_file", - DOWNLOAD: "download_file", - UPDOWNLOAD: "updownload", - CLIPBOARD_COPY: 'clipboard_copy', - CLIPBOARD_PASTE: 'clipboard_paste', - CLIPBOARD_COPY_PASTE: 'clipboard_copy_paste' - } - - NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} - CHOICES = [] - for i, j in DB_CHOICES: - CHOICES.append((NAME_MAP[i], j)) - - @classmethod - def value_to_choices(cls, value): - if isinstance(value, list): - return value - value = int(value) - choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] - return choices - - @classmethod - def value_to_choices_display(cls, value): - choices = cls.value_to_choices(value) - return [str(dict(cls.choices())[i]) for i in choices] - - @classmethod - def choices_to_value(cls, value): - if not isinstance(value, list): - return cls.NONE - db_value = [ - cls.NAME_MAP_REVERSE[v] for v in value - if v in cls.NAME_MAP_REVERSE.keys() - ] - if not db_value: - return cls.NONE - - def to_choices(x, y): - return x | y - - result = reduce(to_choices, db_value) - return result - - @classmethod - def choices(cls): - return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] - - class AssetPermission(BasePermission): assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', blank=True, verbose_name=_("System user")) - actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) class Meta: unique_together = [('org_id', 'name')] diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index 2e62a84bf..613b83a33 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -2,6 +2,7 @@ # import uuid +from functools import reduce from django.utils.translation import ugettext_lazy as _ from django.db import models from django.db.models import Q @@ -13,7 +14,7 @@ from common.utils import date_expired_default, lazyproperty from orgs.mixins.models import OrgManager __all__ = [ - 'BasePermission', 'BasePermissionQuerySet' + 'BasePermission', 'BasePermissionQuerySet', 'Action' ] @@ -39,12 +40,87 @@ class BasePermissionManager(OrgManager): return self.get_queryset().valid() +class Action: + NONE = 0 + + CONNECT = 0b1 + UPLOAD = 0b1 << 1 + DOWNLOAD = 0b1 << 2 + CLIPBOARD_COPY = 0b1 << 3 + CLIPBOARD_PASTE = 0b1 << 4 + ALL = 0xff + UPDOWNLOAD = UPLOAD | DOWNLOAD + CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE + + DB_CHOICES = ( + (ALL, _('All')), + (CONNECT, _('Connect')), + (UPLOAD, _('Upload file')), + (DOWNLOAD, _('Download file')), + (UPDOWNLOAD, _("Upload download")), + (CLIPBOARD_COPY, _('Clipboard copy')), + (CLIPBOARD_PASTE, _('Clipboard paste')), + (CLIPBOARD_COPY_PASTE, _('Clipboard copy paste')) + ) + + NAME_MAP = { + ALL: "all", + CONNECT: "connect", + UPLOAD: "upload_file", + DOWNLOAD: "download_file", + UPDOWNLOAD: "updownload", + CLIPBOARD_COPY: 'clipboard_copy', + CLIPBOARD_PASTE: 'clipboard_paste', + CLIPBOARD_COPY_PASTE: 'clipboard_copy_paste' + } + + NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} + CHOICES = [] + for i, j in DB_CHOICES: + CHOICES.append((NAME_MAP[i], j)) + + @classmethod + def value_to_choices(cls, value): + if isinstance(value, list): + return value + value = int(value) + choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] + return choices + + @classmethod + def value_to_choices_display(cls, value): + choices = cls.value_to_choices(value) + return [str(dict(cls.choices())[i]) for i in choices] + + @classmethod + def choices_to_value(cls, value): + if not isinstance(value, list): + return cls.NONE + db_value = [ + cls.NAME_MAP_REVERSE[v] for v in value + if v in cls.NAME_MAP_REVERSE.keys() + ] + if not db_value: + return cls.NONE + + def to_choices(x, y): + return x | y + + result = reduce(to_choices, db_value) + return result + + @classmethod + def choices(cls): + return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] + + class BasePermission(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss') user_groups = models.ManyToManyField( 'users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss') + actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) is_active = models.BooleanField(default=True, verbose_name=_('Active')) date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired')) diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index 9fb257580..a4a701773 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -1,5 +1,6 @@ # coding: utf-8 # +from .base import * from .asset import * from .application import * from .system_user_permission import * diff --git a/apps/perms/serializers/application/permission.py b/apps/perms/serializers/application/permission.py index e636eec8c..fd886bb17 100644 --- a/apps/perms/serializers/application/permission.py +++ b/apps/perms/serializers/application/permission.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import ApplicationPermission +from ..base import ActionsField __all__ = [ 'ApplicationPermissionSerializer' @@ -13,6 +14,7 @@ __all__ = [ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): + actions = ActionsField(required=False, allow_null=True, label=_("Actions")) category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) is_valid = serializers.BooleanField(read_only=True, label=_('Is valid')) @@ -23,6 +25,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): fields_mini = ['id', 'name'] fields_small = fields_mini + [ 'category', 'category_display', 'type', 'type_display', + 'actions', 'is_active', 'is_expired', 'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket' ] @@ -43,6 +46,24 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): 'applications_amount': {'label': _('Applications amount')}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_actions_choices() + + def set_actions_choices(self): + actions = self.fields.get('actions') + if not actions: + return + choices = actions._choices + if request := self.context.get('request'): + category = request.query_params.get('category') + else: + category = None + exclude_choices = ApplicationPermission.get_exclude_actions_choices(category=category) + for choice in exclude_choices: + choices.pop(choice, None) + actions._choices = choices + @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index 3df2c1173..0b0d5aaa6 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -9,32 +9,9 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action from assets.models import Asset, Node, SystemUser from users.models import User, UserGroup +from ..base import ActionsField -__all__ = [ - 'AssetPermissionSerializer', - 'ActionsField', -] - - -class ActionsField(serializers.MultipleChoiceField): - def __init__(self, *args, **kwargs): - kwargs['choices'] = Action.CHOICES - super().__init__(*args, **kwargs) - - def to_representation(self, value): - return Action.value_to_choices(value) - - def to_internal_value(self, data): - if data is None: - return data - return Action.choices_to_value(data) - - -class ActionsDisplayField(ActionsField): - def to_representation(self, value): - values = super().to_representation(value) - choices = dict(Action.CHOICES) - return [choices.get(i) for i in values] +__all__ = ['AssetPermissionSerializer'] class AssetPermissionSerializer(BulkOrgResourceModelSerializer): diff --git a/apps/perms/serializers/asset/user_permission.py b/apps/perms/serializers/asset/user_permission.py index 0603a0e08..d2ad97894 100644 --- a/apps/perms/serializers/asset/user_permission.py +++ b/apps/perms/serializers/asset/user_permission.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from assets.models import Node, SystemUser, Asset, Platform from assets.serializers import ProtocolsField -from perms.serializers.asset.permission import ActionsField +from perms.serializers.base import ActionsField __all__ = [ 'NodeGrantedSerializer', diff --git a/apps/perms/serializers/base.py b/apps/perms/serializers/base.py new file mode 100644 index 000000000..7e8e1b63f --- /dev/null +++ b/apps/perms/serializers/base.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from perms.models import Action + +__all__ = ['ActionsDisplayField', 'ActionsField'] + + +class ActionsField(serializers.MultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['choices'] = Action.CHOICES + super().__init__(*args, **kwargs) + + def to_representation(self, value): + return Action.value_to_choices(value) + + def to_internal_value(self, data): + if data is None: + return data + return Action.choices_to_value(data) + + +class ActionsDisplayField(ActionsField): + def to_representation(self, value): + values = super().to_representation(value) + choices = dict(Action.CHOICES) + return [choices.get(i) for i in values] + diff --git a/apps/perms/utils/application/permission.py b/apps/perms/utils/application/permission.py index c4ebb5bdb..3f3dc8018 100644 --- a/apps/perms/utils/application/permission.py +++ b/apps/perms/utils/application/permission.py @@ -3,7 +3,7 @@ import time from django.db.models import Q from common.utils import get_logger -from perms.models import ApplicationPermission +from perms.models import ApplicationPermission, Action logger = get_logger(__file__) @@ -33,31 +33,38 @@ def get_user_all_app_perm_ids(user) -> set: return app_perm_ids -def validate_permission(user, application, system_user): +def validate_permission(user, application, system_user, action='connect'): app_perm_ids = get_user_all_app_perm_ids(user) app_perm_ids = ApplicationPermission.applications.through.objects.filter( applicationpermission_id__in=app_perm_ids, application_id=application.id ).values_list('applicationpermission_id', flat=True) - app_perm_ids = set(app_perm_ids) - app_perm_ids = ApplicationPermission.system_users.through.objects.filter( applicationpermission_id__in=app_perm_ids, systemuser_id=system_user.id ).values_list('applicationpermission_id', flat=True) - app_perm_ids = set(app_perm_ids) - - app_perm = ApplicationPermission.objects.filter( + app_perms = ApplicationPermission.objects.filter( id__in=app_perm_ids - ).order_by('-date_expired').first() + ).order_by('-date_expired') - app_perm: ApplicationPermission - if app_perm: - return True, app_perm.date_expired.timestamp() + if app_perms: + actions = set() + actions_values = app_perms.values_list('actions', flat=True) + for value in actions_values: + _actions = Action.value_to_choices(value) + actions.update(_actions) + actions = list(actions) + app_perm: ApplicationPermission = app_perms.first() + expire_at = app_perm.date_expired.timestamp() else: - return False, time.time() + actions = [] + expire_at = time.time() + + # TODO: 组件改造API完成后统一通过actions判断has_perm + has_perm = action in actions + return has_perm, actions, expire_at def get_application_system_user_ids(user, application): diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py index 349040b99..e749e630b 100644 --- a/apps/perms/utils/asset/permission.py +++ b/apps/perms/utils/asset/permission.py @@ -11,7 +11,7 @@ from perms.utils.asset.user_permission import get_user_all_asset_perm_ids logger = get_logger(__file__) -def validate_permission(user, asset, system_user, action_name): +def validate_permission(user, asset, system_user, action='connect'): if not system_user.protocol in asset.protocols_as_dict.keys(): return False, time.time() @@ -50,10 +50,22 @@ def validate_permission(user, asset, system_user, action_name): id__in=asset_perm_ids ).order_by('-date_expired') - for asset_perm in asset_perms: - if action_name in Action.value_to_choices(asset_perm.actions): - return True, asset_perm.date_expired.timestamp() - return False, time.time() + if asset_perms: + actions = set() + actions_values = asset_perms.values_list('actions', flat=True) + for value in actions_values: + _actions = Action.value_to_choices(value) + actions.update(_actions) + asset_perm: AssetPermission = asset_perms.first() + actions = list(actions) + expire_at = asset_perm.date_expired.timestamp() + else: + actions = [] + expire_at = time.time() + + # TODO: 组件改造API完成后统一通过actions判断has_perm + has_perm = action in actions + return has_perm, actions, expire_at def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset): diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py index b03fe9231..ddd77d769 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from perms.serializers import ActionsField +from perms.serializers.base import ActionsField from perms.models import AssetPermission from orgs.utils import tmp_to_org from tickets.models import Ticket