diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 36f734030..8c71ed367 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,11 +1,12 @@ -from .mixin import * -from .category import * -from .platform import * -from .asset import * -from .label import * from .account import * -from .node import * -from .domain import * +from .asset import * from .automations import * -from .gathered_user import * +from .category import * +from .domain import * from .favorite_asset import * +from .gathered_user import * +from .label import * +from .mixin import * +from .node import * +from .platform import * +from .tree import * diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 85935dec2..b45b2170c 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,43 +1,37 @@ # ~*~ coding: utf-8 ~*~ -from functools import partial from collections import namedtuple, defaultdict -from django.core.exceptions import PermissionDenied +from functools import partial -from rest_framework import status -from rest_framework.generics import get_object_or_404 -from rest_framework.serializers import ValidationError -from rest_framework.response import Response -from rest_framework.decorators import action -from django.utils.translation import ugettext_lazy as _ from django.db.models.signals import m2m_changed +from django.utils.translation import ugettext_lazy as _ +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response +from rest_framework.serializers import ValidationError -from common.const.http import POST -from common.exceptions import SomeoneIsDoingThis -from common.const.signals import PRE_REMOVE, POST_REMOVE -from common.mixins.api import SuggestionMixin from assets.models import Asset +from common.const.http import POST +from common.const.signals import PRE_REMOVE, POST_REMOVE +from common.exceptions import SomeoneIsDoingThis +from common.mixins.api import SuggestionMixin from common.utils import get_logger -from common.tree import TreeNodeSerializer -from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics +from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org +from .. import serializers from ..models import Node from ..tasks import ( update_node_assets_hardware_info_manual, test_node_assets_connectivity_manual, check_node_assets_amount_task ) -from .. import serializers -from ..const import AllTypes -from .mixin import SerializeToTreeNodeMixin -from assets.locks import NodeAddChildrenLock logger = get_logger(__file__) __all__ = [ - 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', - 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi', - 'NodeAddChildrenApi', 'NodeListAsTreeApi', 'NodeChildrenAsTreeApi', - 'NodeTaskCreateApi', 'CategoryTreeApi', + 'NodeViewSet', 'NodeAssetsApi', 'NodeAddAssetsApi', + 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi', + 'NodeAddChildrenApi', 'NodeTaskCreateApi', ] @@ -74,153 +68,6 @@ class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet): return super().destroy(request, *args, **kwargs) -class NodeListAsTreeApi(generics.ListAPIView): - """ - 获取节点列表树 - [ - { - "id": "", - "name": "", - "pId": "", - "meta": "" - } - ] - """ - model = Node - serializer_class = TreeNodeSerializer - - @staticmethod - def to_tree_queryset(queryset): - queryset = [node.as_tree_node() for node in queryset] - return queryset - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.to_tree_queryset(queryset) - return queryset - - -class NodeChildrenApi(generics.ListCreateAPIView): - serializer_class = serializers.NodeSerializer - search_fields = ('value',) - - instance = None - is_initial = False - - def initial(self, request, *args, **kwargs): - self.instance = self.get_object() - return super().initial(request, *args, **kwargs) - - def perform_create(self, serializer): - with NodeAddChildrenLock(self.instance): - data = serializer.validated_data - _id = data.get("id") - value = data.get("value") - if not value: - value = self.instance.get_next_child_preset_name() - node = self.instance.create_child(value=value, _id=_id) - # 避免查询 full value - node._full_value = node.value - serializer.instance = node - - def get_object(self): - pk = self.kwargs.get('pk') or self.request.query_params.get('id') - key = self.request.query_params.get("key") - - if not pk and not key: - self.is_initial = True - if current_org.is_root(): - node = None - else: - node = Node.org_root() - return node - if pk: - node = get_object_or_404(Node, pk=pk) - else: - node = get_object_or_404(Node, key=key) - return node - - def get_org_root_queryset(self, query_all): - if query_all: - return Node.objects.all() - else: - return Node.org_root_nodes() - - def get_queryset(self): - query_all = self.request.query_params.get("all", "0") == "all" - - if self.is_initial and current_org.is_root(): - return self.get_org_root_queryset(query_all) - - if self.is_initial: - with_self = True - else: - with_self = False - - if not self.instance: - return Node.objects.none() - - if query_all: - queryset = self.instance.get_all_children(with_self=with_self) - else: - queryset = self.instance.get_children(with_self=with_self) - return queryset - - -class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): - """ - 节点子节点作为树返回, - [ - { - "id": "", - "name": "", - "pId": "", - "meta": "" - } - ] - - """ - model = Node - - def filter_queryset(self, queryset): - if not self.request.GET.get('search'): - return queryset - queryset = super().filter_queryset(queryset) - queryset = self.model.get_ancestor_queryset(queryset) - return queryset - - def list(self, request, *args, **kwargs): - nodes = self.filter_queryset(self.get_queryset()).order_by('value') - nodes = self.serialize_nodes(nodes, with_asset_amount=True) - assets = self.get_assets() - data = [*nodes, *assets] - return Response(data=data) - - def get_assets(self): - include_assets = self.request.query_params.get('assets', '0') == '1' - if not self.instance or not include_assets: - return [] - assets = self.instance.get_assets().only( - "id", "name", "address", "platform_id", - "org_id", "is_active", - ).prefetch_related('platform') - return self.serialize_assets(assets, self.instance.key) - - -class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): - serializer_class = TreeNodeSerializer - - def check_permissions(self, request): - if not request.user.has_perm('assets.view_asset'): - raise PermissionDenied - return True - - def list(self, request, *args, **kwargs): - nodes = AllTypes.to_tree_nodes() - serializer = self.get_serializer(nodes, many=True) - return Response(data=serializer.data) - - class NodeAssetsApi(generics.ListAPIView): serializer_class = serializers.AssetSerializer diff --git a/apps/assets/api/tree.py b/apps/assets/api/tree.py new file mode 100644 index 000000000..a635fef0b --- /dev/null +++ b/apps/assets/api/tree.py @@ -0,0 +1,173 @@ +# ~*~ coding: utf-8 ~*~ + +from django.core.exceptions import PermissionDenied +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response + +from assets.locks import NodeAddChildrenLock +from common.tree import TreeNodeSerializer +from common.utils import get_logger +from orgs.mixins import generics +from orgs.utils import current_org +from .mixin import SerializeToTreeNodeMixin +from .. import serializers +from ..const import AllTypes +from ..models import Node + +logger = get_logger(__file__) +__all__ = [ + 'NodeChildrenApi', + 'NodeListAsTreeApi', + 'NodeChildrenAsTreeApi', + 'CategoryTreeApi', +] + + +class NodeListAsTreeApi(generics.ListAPIView): + """ + 获取节点列表树 + [ + { + "id": "", + "name": "", + "pId": "", + "meta": "" + } + ] + """ + model = Node + serializer_class = TreeNodeSerializer + + @staticmethod + def to_tree_queryset(queryset): + queryset = [node.as_tree_node() for node in queryset] + return queryset + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.to_tree_queryset(queryset) + return queryset + + +class NodeChildrenApi(generics.ListCreateAPIView): + serializer_class = serializers.NodeSerializer + search_fields = ('value',) + + instance = None + is_initial = False + + def initial(self, request, *args, **kwargs): + self.instance = self.get_object() + return super().initial(request, *args, **kwargs) + + def perform_create(self, serializer): + with NodeAddChildrenLock(self.instance): + data = serializer.validated_data + _id = data.get("id") + value = data.get("value") + if not value: + value = self.instance.get_next_child_preset_name() + node = self.instance.create_child(value=value, _id=_id) + # 避免查询 full value + node._full_value = node.value + serializer.instance = node + + def get_object(self): + pk = self.kwargs.get('pk') or self.request.query_params.get('id') + key = self.request.query_params.get("key") + + if not pk and not key: + self.is_initial = True + if current_org.is_root(): + node = None + else: + node = Node.org_root() + return node + if pk: + node = get_object_or_404(Node, pk=pk) + else: + node = get_object_or_404(Node, key=key) + return node + + def get_org_root_queryset(self, query_all): + if query_all: + return Node.objects.all() + else: + return Node.org_root_nodes() + + def get_queryset(self): + query_all = self.request.query_params.get("all", "0") == "all" + + if self.is_initial and current_org.is_root(): + return self.get_org_root_queryset(query_all) + + if self.is_initial: + with_self = True + else: + with_self = False + + if not self.instance: + return Node.objects.none() + + if query_all: + queryset = self.instance.get_all_children(with_self=with_self) + else: + queryset = self.instance.get_children(with_self=with_self) + return queryset + + +class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): + """ + 节点子节点作为树返回, + [ + { + "id": "", + "name": "", + "pId": "", + "meta": "" + } + ] + + """ + model = Node + + def filter_queryset(self, queryset): + if not self.request.GET.get('search'): + return queryset + queryset = super().filter_queryset(queryset) + queryset = self.model.get_ancestor_queryset(queryset) + return queryset + + def list(self, request, *args, **kwargs): + nodes = self.filter_queryset(self.get_queryset()).order_by('value') + nodes = self.serialize_nodes(nodes, with_asset_amount=True) + assets = self.get_assets() + data = [*nodes, *assets] + return Response(data=data) + + def get_assets(self): + include_assets = self.request.query_params.get('assets', '0') == '1' + if not self.instance or not include_assets: + return [] + assets = self.instance.get_assets().only( + "id", "name", "address", "platform_id", + "org_id", "is_active", + ).prefetch_related('platform') + return self.serialize_assets(assets, self.instance.key) + + +class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): + serializer_class = TreeNodeSerializer + + def check_permissions(self, request): + if not request.user.has_perm('assets.view_asset'): + raise PermissionDenied + return True + + def list(self, request, *args, **kwargs): + if request.query_params.get('key'): + nodes = [] + else: + nodes = AllTypes.to_tree_nodes() + serializer = self.get_serializer(nodes, many=True) + return Response(data=serializer.data) diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index f9cf83b85..c9012cd8c 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -1,14 +1,14 @@ +from collections import defaultdict from copy import deepcopy from common.db.models import ChoicesMixin from common.tree import TreeNode - from .category import Category -from .host import HostTypes -from .device import DeviceTypes -from .database import DatabaseTypes -from .web import WebTypes from .cloud import CloudTypes +from .database import DatabaseTypes +from .device import DeviceTypes +from .host import HostTypes +from .web import WebTypes class AllTypes(ChoicesMixin): @@ -54,7 +54,7 @@ class AllTypes(ChoicesMixin): item_name = item.replace('_enabled', '') methods = filter_platform_methods(category, tp, item_name) methods = [{'name': m['name'], 'id': m['id']} for m in methods] - automation_methods[item_name+'_methods'] = methods + automation_methods[item_name + '_methods'] = methods automation.update(automation_methods) constraints['automation'] = automation return constraints @@ -124,7 +124,7 @@ class AllTypes(ChoicesMixin): @staticmethod def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None): node = TreeNode(**{ - 'id': choice.name, + 'id': pid + '_' + choice.name, 'name': choice.label, 'title': choice.label, 'pId': pid, @@ -135,16 +135,57 @@ class AllTypes(ChoicesMixin): node.meta = meta return node + @classmethod + def platform_to_node(cls, p, pid): + node = TreeNode(**{ + 'id': '{}'.format(p.id), + 'name': p.name, + 'title': p.name, + 'pId': pid, + 'isParent': True, + 'meta': { + 'type': 'platform' + } + }) + return node + @classmethod def to_tree_nodes(cls): - root = TreeNode(id='ROOT', name='类型节点', title='类型节点') + from ..models import Asset, Platform + asset_platforms = Asset.objects.all().values_list('platform_id', flat=True) + platform_count = defaultdict(int) + for platform_id in asset_platforms: + platform_count[platform_id] += 1 + + category_type_mapper = defaultdict(int) + platforms = Platform.objects.all() + tp_platforms = defaultdict(list) + + for p in platforms: + category_type_mapper[p.category + '_' + p.type] += platform_count[p.id] + category_type_mapper[p.category] += platform_count[p.id] + tp_platforms[p.category + '_' + p.type].append(p) + + root = TreeNode(id='ROOT', name='所有类型', title='所有类型', open=True, isParent=True) nodes = [root] for category, types in cls.category_types(): - category_node = cls.choice_to_node(category, 'ROOT', meta={'type': 'category'}) + meta = {'type': 'category', 'category': category.value} + category_node = cls.choice_to_node(category, 'ROOT', meta=meta) + category_count = category_type_mapper.get(category, 0) + category_node.name += f'({category_count})' nodes.append(category_node) + for tp in types: - tp_node = cls.choice_to_node(tp, category_node.id, meta={'type': 'type'}) + meta = {'type': 'type', 'category': category.value, '_type': tp.value} + tp_node = cls.choice_to_node(tp, category_node.id, opened=False, meta=meta) + tp_count = category_type_mapper.get(category + '_' + tp, 0) + tp_node.name += f'({tp_count})' nodes.append(tp_node) + + for p in tp_platforms.get(category + '_' + tp, []): + platform_node = cls.platform_to_node(p, tp_node.id) + platform_node.name += f'({platform_count.get(p.id, 0)})' + nodes.append(platform_node) return nodes @classmethod @@ -253,8 +294,3 @@ class AllTypes(ChoicesMixin): print("\t- Update platform: {}".format(platform.name)) platform_data = cls.get_type_default_platform(platform.category, platform.type) cls.create_or_update_by_platform_data(platform.name, platform_data) - - - - - diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index dbdcd21fd..b3742682d 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords from common.utils import lazyproperty - from .base import AbsConnectivity, BaseAccount __all__ = ['Account', 'AccountTemplate'] @@ -74,6 +73,12 @@ class Account(AbsConnectivity, BaseAccount): def platform(self): return self.asset.platform + @lazyproperty + def alias(self): + if self.username.startswith('@'): + return self.username + return self.name + def __str__(self): return '{}'.format(self.username) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 60528066e..c77a04fa7 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -266,7 +266,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView permed_account = util.validate_permission(user, asset, account_name) if not permed_account or not permed_account.actions: - msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( + msg = 'user `{}` not has asset `{}` permission for account `{}`'.format( user, asset, account_name ) raise PermissionDenied(msg) diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py index c9abeee6c..850d1385b 100644 --- a/apps/perms/api/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -6,14 +6,10 @@ from django.db.models import Q from rest_framework.generics import ListAPIView from rest_framework.response import Response -from common.utils import lazyproperty -from perms.models import AssetPermission -from assets.models import Asset, Node -from . import user_permission as uapi -from perms import serializers -from perms.utils import PermAccountUtil from assets.api.mixin import SerializeToTreeNodeMixin -from users.models import UserGroup +from assets.models import Asset, Node +from perms import serializers +from perms.models import AssetPermission __all__ = [ 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', @@ -101,11 +97,11 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): granted_node_q |= Q(nodes__key=_key) granted_asset_q = ( - Q(granted_by_permissions__id__in=asset_perm_ids) & - ( - Q(nodes__key__startswith=f'{node.key}:') | - Q(nodes__key=node.key) - ) + Q(granted_by_permissions__id__in=asset_perm_ids) & + ( + Q(nodes__key__startswith=f'{node.key}:') | + Q(nodes__key=node.key) + ) ) assets = Asset.objects.filter( @@ -115,7 +111,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): class UserGroupGrantedNodesApi(ListAPIView): - serializer_class = serializers.NodeGrantedSerializer + serializer_class = serializers.NodePermedSerializer rbac_perms = { 'list': 'perms.view_usergroupassets', } diff --git a/apps/perms/api/user_permission/nodes.py b/apps/perms/api/user_permission/nodes.py index 2fcfa5e2c..1972909e9 100644 --- a/apps/perms/api/user_permission/nodes.py +++ b/apps/perms/api/user_permission/nodes.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- # import abc + from rest_framework.generics import ListAPIView from assets.models import Node +from common.utils import get_logger, lazyproperty from perms import serializers from perms.utils.user_permission import UserGrantedNodesQueryUtils -from common.utils import get_logger, lazyproperty - from .mixin import SelfOrPKUserMixin logger = get_logger(__name__) @@ -19,7 +19,7 @@ __all__ = [ class BaseUserPermedNodesApi(SelfOrPKUserMixin, ListAPIView): - serializer_class = serializers.NodeGrantedSerializer + serializer_class = serializers.NodePermedSerializer def get_queryset(self): if getattr(self, 'swagger_fake_view', False): @@ -37,12 +37,14 @@ class BaseUserPermedNodesApi(SelfOrPKUserMixin, ListAPIView): class UserAllPermedNodesApi(BaseUserPermedNodesApi): """ 用户授权的节点 """ + def get_nodes(self): return self.query_node_util.get_whole_tree_nodes() class UserPermedNodeChildrenApi(BaseUserPermedNodesApi): """ 用户授权的节点下的子节点 """ + def get_nodes(self): key = self.request.query_params.get('key') nodes = self.query_node_util.get_node_children(key) diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index ba0619edc..9b4ad26f0 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -11,7 +11,7 @@ from common.drf.fields import ObjectRelatedField, LabeledChoiceField from perms.serializers.permission import ActionChoicesField __all__ = [ - 'NodeGrantedSerializer', 'AssetPermedSerializer', + 'NodePermedSerializer', 'AssetPermedSerializer', 'AccountsPermedSerializer' ] @@ -26,15 +26,14 @@ class AssetPermedSerializer(serializers.ModelSerializer): class Meta: model = Asset only_fields = [ - "id", "name", "address", - 'domain', 'platform', + "id", "name", "address", 'domain', 'platform', "comment", "org_id", "is_active", ] fields = only_fields + ['protocols', 'category', 'type', 'specific'] + ['org_name'] read_only_fields = fields -class NodeGrantedSerializer(serializers.ModelSerializer): +class NodePermedSerializer(serializers.ModelSerializer): class Meta: model = Node fields = [ @@ -48,6 +47,8 @@ class AccountsPermedSerializer(serializers.ModelSerializer): class Meta: model = Account - fields = ['id', 'name', 'has_username', 'username', - 'has_secret', 'secret_type', 'actions'] + fields = [ + 'alias', 'name', 'username', 'has_username', + 'has_secret', 'secret_type', 'actions' + ] read_only_fields = fields diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index 7c0caf988..d4ce2e402 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -16,7 +16,7 @@ class PermAccountUtil(AssetPermissionUtil): :param account_name: 可能是 @USER @INPUT 字符串 """ permed_accounts = self.get_permed_accounts_for_user(user, asset) - accounts_mapper = {account.name: account for account in permed_accounts} + accounts_mapper = {account.alias: account for account in permed_accounts} account = accounts_mapper.get(account_name) return account