From 1457281b73980460f0b847b500aa5771fe475d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Tue, 17 Sep 2019 12:34:47 +0800 Subject: [PATCH] Bugfix (#3232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 拆分user permission * [Update] 修改 ops command * [Update] 修改setting无法生效的问题 * [Update] 修复授权详情-授权资产或节点添加资产失败 * [Bugfix] 修复组织管理员运行命令时的问题 * [Update] 修复命令执行左侧树点击问题 --- .../templates/assets/_asset_list_modal.html | 2 +- apps/common/apps.py | 13 +- apps/ops/api/command.py | 6 +- .../ops/command_execution_create.html | 634 +++++++++--------- apps/ops/views/command.py | 6 +- apps/perms/api/mixin.py | 154 +---- apps/perms/api/user_permission.py | 321 --------- apps/perms/api/user_permission/__init__.py | 6 + apps/perms/api/user_permission/common.py | 113 ++++ apps/perms/api/user_permission/mixin.py | 70 ++ .../user_permission/user_permission_assets.py | 66 ++ .../user_permission/user_permission_nodes.py | 82 +++ .../user_permission_nodes_with_assets.py | 56 ++ .../perms/asset_permission_asset.html | 6 +- apps/perms/utils/asset_permission.py | 43 +- apps/settings/signals_handler.py | 2 +- 16 files changed, 767 insertions(+), 813 deletions(-) delete mode 100644 apps/perms/api/user_permission.py create mode 100644 apps/perms/api/user_permission/__init__.py create mode 100644 apps/perms/api/user_permission/common.py create mode 100644 apps/perms/api/user_permission/mixin.py create mode 100644 apps/perms/api/user_permission/user_permission_assets.py create mode 100644 apps/perms/api/user_permission/user_permission_nodes.py create mode 100644 apps/perms/api/user_permission/user_permission_nodes_with_assets.py diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index 06aa4a707..8d8c3f0ba 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -92,7 +92,7 @@ function syncSelectedAssetsToModalTable(assetModalTable) { } // input assets有,table assets没选,则选中(click) - if (inputAssets !== null) { + if (inputAssets) { assetModalTable.selected = inputAssets; $.each(inputAssets, function (index, assetId) { var dom = document.getElementById(assetId); diff --git a/apps/common/apps.py b/apps/common/apps.py index 9d4d80677..bf9348826 100644 --- a/apps/common/apps.py +++ b/apps/common/apps.py @@ -2,16 +2,6 @@ from __future__ import unicode_literals import sys from django.apps import AppConfig -from django.dispatch import receiver -from django.db.backends.signals import connection_created - - -@receiver(connection_created) -def on_db_connection_ready(sender, **kwargs): - from .signals import django_ready - if 'migrate' not in sys.argv: - django_ready.send(CommonConfig) - connection_created.disconnect(on_db_connection_ready) class CommonConfig(AppConfig): @@ -19,3 +9,6 @@ class CommonConfig(AppConfig): def ready(self): from . import signals_handlers + from .signals import django_ready + if 'migrate' not in sys.argv: + django_ready.send(CommonConfig) diff --git a/apps/ops/api/command.py b/apps/ops/api/command.py index 6712e97c6..f4deecc1a 100644 --- a/apps/ops/api/command.py +++ b/apps/ops/api/command.py @@ -30,10 +30,10 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet): util = AssetPermissionUtilV2(self.request.user) util.filter_permissions(system_users=system_user.id) permed_assets = util.get_assets().filter(id__in=[a.id for a in assets]) - unpermed_assets = set(assets) - set(permed_assets) - if unpermed_assets: + invalid_assets = set(assets) - set(permed_assets) + if invalid_assets: msg = _("Not has host {} permission").format( - [str(a.id) for a in unpermed_assets] + [str(a.id) for a in invalid_assets] ) raise ValidationError({"hosts": msg}) diff --git a/apps/ops/templates/ops/command_execution_create.html b/apps/ops/templates/ops/command_execution_create.html index 424066a6e..19936154f 100644 --- a/apps/ops/templates/ops/command_execution_create.html +++ b/apps/ops/templates/ops/command_execution_create.html @@ -4,347 +4,365 @@ {% load bootstrap3 %} {% block custom_head_css_js %} - - - - - - - + + + + + + + - + {% endblock %} {% block content %} -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- - -
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {% trans 'Loading' %} .. +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+
{% endblock %} {% block custom_foot_js %} - + function execute() { + if (!term) { + initResultTerminal() + } + var size = 'rows=' + term.rows + '&cols=' + term.cols; + var url = '{% url "api-ops:command-execution-list" %}?' + size; + var run_as = systemUserId; + var command = editor.getValue(); + var hosts = getSelectedAssetsNode().map(function (node) { + return node.id; + }); + if (hosts.length === 0) { + term.write(wrapperError("{% trans 'Unselected assets' %}")); + return + } + if (!command) { + term.write(wrapperError("{% trans 'No input command' %}")); + return + } + if (!run_as) { + term.write(wrapperError("{% trans 'No system user was selected' %}")); + return + } + var data = { + hosts: hosts, + run_as: run_as, + command: command + }; + var mark = ''; + var log_url = null; + var end = false; + var error = false; + var int = null; + var interval = 200; + + function writeExecutionOutput() { + if (!end) { + $.ajax({ + url: log_url + '?mark=' + mark, + method: "GET", + contentType: "application/json; charset=utf-8" + }).done(function (data, textStatue, jqXHR) { + if (jqXHR.status === 203) { + error = true; + term.write('.'); + interval = 500; + } + if (jqXHR.status === 200) { + term.write(data.data); + mark = data.mark; + if (data.end) { + end = true; + window.clearInterval(int) + } + } + }) + } + } + + requestApi({ + url: url, + body: JSON.stringify(data), + method: 'POST', + flash_message: false, + success: function (resp) { + var msg = "{% trans 'Pending' %}"; + term.write(msg + "...\r\n"); + log_url = resp.log_url; + int = setInterval(function () { + writeExecutionOutput() + }, interval); + } + }); + return false; + } + + var editor; + $(document).ready(function () { + systemUserId = $('#system-users-select').val(); + $(".select2").select2({ + dropdownAutoWidth: true, + }).on('select2:select', function (evt) { + var data = evt.params.data; + systemUserId = data.id; + initTree(); + }); + editor = CodeMirror.fromTextArea(document.getElementById("command-text"), { + lineNumbers: true, + lineWrapping: true, + mode: "shell" + }); + editor.setSize(600, 100); + var charWidth = editor.defaultCharWidth(), basePadding = 4; + editor.on("renderLine", function (cm, line, elt) { + var off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidth; + elt.style.textIndent = "-" + off + "px"; + elt.style.paddingLeft = (basePadding + off) + "px"; + }); + editor.refresh(); + initTree(); + initResultTerminal(); + }).on('click', '.btn-execute', function () { + execute() + }) + {% endblock %} \ No newline at end of file diff --git a/apps/ops/views/command.py b/apps/ops/views/command.py index 1d5420c9f..0b0ec6542 100644 --- a/apps/ops/views/command.py +++ b/apps/ops/views/command.py @@ -9,6 +9,7 @@ from common.permissions import ( PermissionsMixin, IsOrgAdmin, IsValidUser, IsOrgAuditor ) from common.mixins import DatetimeSearchMixin +from orgs.utils import tmp_to_root_org from ..models import CommandExecution from ..forms import CommandExecutionForm @@ -67,8 +68,9 @@ class CommandExecutionStartView(PermissionsMixin, TemplateView): def get_user_system_users(self): from perms.utils import AssetPermissionUtilV2 user = self.request.user - util = AssetPermissionUtilV2(user) - system_users = [s for s in util.get_system_users() if s.protocol == 'ssh'] + with tmp_to_root_org(): + util = AssetPermissionUtilV2(user) + system_users = [s for s in util.get_system_users() if s.protocol == 'ssh'] return system_users def get_context_data(self, **kwargs): diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py index e2cdb139b..dd5678a7c 100644 --- a/apps/perms/api/mixin.py +++ b/apps/perms/api/mixin.py @@ -1,29 +1,20 @@ # -*- coding: utf-8 -*- # import uuid -from hashlib import md5 -from django.core.cache import cache from django.db.models import Q -from django.conf import settings -from rest_framework.views import Response -from django.utils.decorators import method_decorator -from django.views.decorators.http import condition from rest_framework.generics import get_object_or_404 -from django.utils.translation import ugettext as _ from assets.utils import LabelFilterMixin -from common.permissions import IsValidUser, IsOrgAdminOrAppUser, IsOrgAdmin +from common.permissions import IsValidUser, IsOrgAdminOrAppUser from common.utils import get_logger from orgs.utils import set_to_root_org -from ..hands import User, Asset, Node, SystemUser +from ..hands import User, Asset, SystemUser from .. import serializers -from .. import const logger = get_logger(__name__) __all__ = [ - 'UserPermissionCacheMixin', 'GrantAssetsMixin', 'NodesWithUngroupMixin', 'UserPermissionMixin', ] @@ -54,147 +45,6 @@ class UserPermissionMixin: return super().get_permissions() -# def get_etag(request, *args, **kwargs): -# cache_policy = request.GET.get("cache_policy") -# if cache_policy != '1': -# return None -# if not UserPermissionCacheMixin.CACHE_ENABLE: -# return None -# view = request.parser_context.get("view") -# if not view: -# return None -# etag = view.get_meta_cache_id() -# return etag - - -class UserPermissionCacheMixin: - pass -# cache_policy = '0' -# RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_V2_{}' -# CACHE_ENABLE = settings.ASSETS_PERM_CACHE_ENABLE -# CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME -# _object = None -# -# def get_object(self): -# return None -# -# # 内部使用可控制缓存 -# def _get_object(self): -# if not self._object: -# self._object = self.get_object() -# return self._object -# -# def get_object_id(self): -# obj = self._get_object() -# if obj: -# return str(obj.id) -# return None -# -# def get_request_md5(self): -# path = self.request.path -# query = {k: v for k, v in self.request.GET.items()} -# query.pop("_", None) -# query = "&".join(["{}={}".format(k, v) for k, v in query.items()]) -# full_path = "{}?{}".format(path, query) -# return md5(full_path.encode()).hexdigest() -# -# def get_meta_cache_id(self): -# obj = self._get_object() -# util = AssetPermissionUtil(obj, cache_policy=self.cache_policy) -# meta_cache_id = util.cache_meta.get('id') -# return meta_cache_id -# -# def get_response_cache_id(self): -# obj_id = self.get_object_id() -# request_md5 = self.get_request_md5() -# meta_cache_id = self.get_meta_cache_id() -# resp_cache_id = '{}_{}_{}'.format(obj_id, request_md5, meta_cache_id) -# return resp_cache_id -# -# def get_response_from_cache(self): -# # 没有数据缓冲 -# meta_cache_id = self.get_meta_cache_id() -# if not meta_cache_id: -# logger.debug("Not get meta id: {}".format(meta_cache_id)) -# return None -# # 从响应缓冲里获取响应 -# key = self.get_response_key() -# data = cache.get(key) -# if not data: -# logger.debug("Not get response from cache: {}".format(key)) -# return None -# logger.debug("Get user permission from cache: {}".format(self.get_object())) -# response = Response(data) -# return response -# -# def expire_response_cache(self): -# obj_id = self.get_object_id() -# expire_cache_id = '{}_{}'.format(obj_id, '*') -# key = self.RESP_CACHE_KEY.format(expire_cache_id) -# cache.delete_pattern(key) -# -# def get_response_key(self): -# resp_cache_id = self.get_response_cache_id() -# key = self.RESP_CACHE_KEY.format(resp_cache_id) -# return key -# -# def set_response_to_cache(self, response): -# key = self.get_response_key() -# cache.set(key, response.data, self.CACHE_TIME) -# logger.debug("Set response to cache: {}".format(key)) -# -# @method_decorator(condition(etag_func=get_etag)) -# def get(self, request, *args, **kwargs): -# if not self.CACHE_ENABLE: -# self.cache_policy = '0' -# else: -# self.cache_policy = request.GET.get('cache_policy', '0') -# -# obj = self._get_object() -# if obj is None: -# logger.debug("Not get response from cache: obj is none") -# return super().get(request, *args, **kwargs) -# -# if AssetPermissionUtil.is_not_using_cache(self.cache_policy): -# logger.debug("Not get resp from cache: {}".format(self.cache_policy)) -# return super().get(request, *args, **kwargs) -# elif AssetPermissionUtil.is_refresh_cache(self.cache_policy): -# logger.debug("Not get resp from cache: {}".format(self.cache_policy)) -# self.expire_response_cache() -# -# logger.debug("Try get response from cache") -# resp = self.get_response_from_cache() -# if not resp: -# resp = super().get(request, *args, **kwargs) -# self.set_response_to_cache(resp) -# return resp - - -class NodesWithUngroupMixin: - util = None - - @staticmethod - def get_ungrouped_node(ungroup_key): - return Node(key=ungroup_key, id=const.UNGROUPED_NODE_ID, - value=_("ungrouped")) - - @staticmethod - def get_empty_node(): - return Node(key=const.EMPTY_NODE_KEY, id=const.EMPTY_NODE_ID, - value=_("empty")) - - def add_ungrouped_nodes(self, node_map, node_keys): - ungroup_key = '1:-1' - for key in node_keys: - if key.endswith('-1'): - ungroup_key = key - break - ungroup_node = self.get_ungrouped_node(ungroup_key) - empty_node = self.get_empty_node() - node_map[ungroup_key] = ungroup_node - node_map[const.EMPTY_NODE_KEY] = empty_node - - class GrantAssetsMixin(LabelFilterMixin): serializer_class = serializers.AssetGrantedSerializer diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py deleted file mode 100644 index 0754e52c8..000000000 --- a/apps/perms/api/user_permission.py +++ /dev/null @@ -1,321 +0,0 @@ -# -*- coding: utf-8 -*- -# -import uuid - -from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView, Response -from rest_framework.generics import ( - ListAPIView, get_object_or_404, RetrieveAPIView -) - -from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin -from common.tree import TreeNodeSerializer -from common.utils import get_logger -from ..utils import ( - ParserNode, AssetPermissionUtilV2 -) -from ..hands import User, Asset, Node, SystemUser, NodeSerializer -from .. import serializers -from ..models import Action -from .mixin import UserPermissionMixin - - -logger = get_logger(__name__) - -__all__ = [ - 'UserGrantedAssetsApi', - 'UserGrantedAssetsAsTreeApi', - 'UserGrantedNodeAssetsApi', - 'UserGrantedNodesApi', - 'UserGrantedNodesAsTreeApi', - 'UserGrantedNodesWithAssetsAsTreeApi', - 'UserGrantedNodeChildrenApi', - 'UserGrantedNodeChildrenAsTreeApi', - 'UserGrantedNodeChildrenWithAssetsAsTreeApi', - 'RefreshAssetPermissionCacheApi', - 'UserGrantedAssetSystemUsersApi', - 'ValidateUserAssetPermissionApi', - 'GetUserAssetPermissionActionsApi', -] - - -class UserAssetPermissionMixin(UserPermissionMixin): - util = None - - def initial(self, *args, **kwargs): - super().initial(*args, *kwargs) - cache_policy = self.request.query_params.get('cache_policy', '0') - self.util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) - - -class UserNodeTreeMixin: - serializer_class = TreeNodeSerializer - nodes_only_fields = ParserNode.nodes_only_fields - tree = None - - def parse_nodes_to_queryset(self, nodes): - nodes = nodes.only(*self.nodes_only_fields) - _queryset = [] - - tree = self.util.get_user_tree() - for node in nodes: - assets_amount = tree.assets_amount(node.key) - if assets_amount == 0: - continue - node._assets_amount = assets_amount - data = ParserNode.parse_node_to_tree_node(node) - _queryset.append(data) - return _queryset - - def get_serializer_queryset(self, queryset): - queryset = self.parse_nodes_to_queryset(queryset) - return queryset - - def get_serializer(self, queryset, many=True, **kwargs): - queryset = self.get_serializer_queryset(queryset) - queryset.sort() - return super().get_serializer(queryset, many=many, **kwargs) - - -class UserAssetTreeMixin: - serializer_class = TreeNodeSerializer - nodes_only_fields = ParserNode.assets_only_fields - - @staticmethod - def parse_assets_to_queryset(assets, node): - _queryset = [] - for asset in assets: - data = ParserNode.parse_asset_to_tree_node(node, asset) - _queryset.append(data) - return _queryset - - def get_serializer_queryset(self, queryset): - queryset = queryset.only(*self.nodes_only_fields) - _queryset = self.parse_assets_to_queryset(queryset, None) - return _queryset - - def get_serializer(self, queryset, many=True, **kwargs): - queryset = self.get_serializer_queryset(queryset) - queryset.sort() - return super().get_serializer(queryset, many=many, **kwargs) - - -class UserGrantedAssetsApi(UserAssetPermissionMixin, ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.AssetGrantedSerializer - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip', 'id', 'comment'] - search_fields = ['hostname', 'ip', 'comment'] - - def filter_by_nodes(self, queryset): - node_id = self.request.query_params.get("node") - if not node_id: - return queryset - node = get_object_or_404(Node, pk=node_id) - query_all = self.request.query_params.get("all", "0") in ["1", "true"] - if query_all: - pattern = '^{0}$|^{0}:'.format(node.key) - queryset = queryset.filter(nodes__key__regex=pattern).distinct() - else: - queryset = queryset.filter(nodes=node) - return queryset - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.filter_by_nodes(queryset) - return queryset - - def get_queryset(self): - queryset = self.util.get_assets().only(*self.only_fields) - return queryset - - -class UserGrantedAssetsAsTreeApi(UserAssetTreeMixin, UserGrantedAssetsApi): - pass - - -class UserGrantedNodeAssetsApi(UserGrantedAssetsApi): - def get_queryset(self): - node_id = self.kwargs.get("node_id") - node = get_object_or_404(Node, pk=node_id) - deep = self.request.query_params.get("all", "0") == "1" - queryset = self.util.get_nodes_assets(node, deep=deep)\ - .only(*self.only_fields) - return queryset - - -class UserGrantedNodesApi(UserAssetPermissionMixin, ListAPIView): - """ - 查询用户授权的所有节点的API - """ - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.NodeGrantedSerializer - nodes_only_fields = NodeSerializer.Meta.only_fields - - def get_serializer_context(self): - context = super().get_serializer_context() - context["tree"] = self.util.user_tree - return context - - def get_queryset(self): - node_keys = self.util.get_nodes() - queryset = Node.objects.filter(key__in=node_keys)\ - .only(*self.nodes_only_fields) - return queryset - - -class UserGrantedNodesAsTreeApi(UserNodeTreeMixin, UserGrantedNodesApi): - pass - - -class UserGrantedNodesWithAssetsAsTreeApi(UserGrantedNodesAsTreeApi): - def get_serializer_queryset(self, queryset): - _queryset = super().get_serializer_queryset(queryset) - for node in queryset: - assets = self.util.get_nodes_assets(node) - _queryset.extend( - UserAssetTreeMixin.parse_assets_to_queryset(assets, node) - ) - return _queryset - - -class UserGrantedNodeChildrenApi(UserGrantedNodesApi): - node = None - tree = None - root_keys = None # 如果是第一次访问,则需要把二级节点添加进去,这个 roots_keys - - def get(self, request, *args, **kwargs): - key = self.request.query_params.get("key") - pk = self.request.query_params.get("id") - system_user_id = self.request.query_params.get("system_user") - if system_user_id: - self.util.filter_permissions(system_users=system_user_id) - self.tree = self.util.get_user_tree() - - node = None - if pk is not None: - node = get_object_or_404(Node, id=pk) - elif key is not None: - node = get_object_or_404(Node, key=key) - self.node = node - return super().get(request, *args, **kwargs) - - def get_queryset(self): - if self.node: - children = self.tree.children(self.node.key) - else: - children = self.tree.children(self.tree.root) - # 默认打开组织节点下的的节点 - self.root_keys = [child.identifier for child in children] - for key in self.root_keys: - children.extend(self.tree.children(key)) - node_keys = [n.identifier for n in children] - queryset = Node.objects.filter(key__in=node_keys) - return queryset - - -class UserGrantedNodeChildrenAsTreeApi(UserNodeTreeMixin, UserGrantedNodeChildrenApi): - pass - - -class UserGrantedNodeChildrenWithAssetsAsTreeApi(UserGrantedNodeChildrenAsTreeApi): - nodes_only_fields = ParserNode.nodes_only_fields - assets_only_fields = ParserNode.assets_only_fields - - def get_serializer_queryset(self, queryset): - _queryset = super().get_serializer_queryset(queryset) - nodes = [] - if self.node: - nodes.append(self.node) - elif self.root_keys: - nodes = Node.objects.filter(key__in=self.root_keys) - - for node in nodes: - assets = self.util.get_nodes_assets(node).only( - *self.assets_only_fields - ) - _queryset.extend( - UserAssetTreeMixin.parse_assets_to_queryset(assets, node) - ) - return _queryset - - -class GetUserAssetPermissionActionsApi(UserAssetPermissionMixin, RetrieveAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.ActionsSerializer - - def get_obj(self): - user_id = self.request.query_params.get('user_id', '') - user = get_object_or_404(User, id=user_id) - return user - - def get_object(self): - asset_id = self.request.query_params.get('asset_id', '') - system_id = self.request.query_params.get('system_user_id', '') - - try: - asset_id = uuid.UUID(asset_id) - system_id = uuid.UUID(system_id) - except ValueError: - return Response({'msg': False}, status=403) - - asset = get_object_or_404(Asset, id=asset_id) - system_user = get_object_or_404(SystemUser, id=system_id) - - system_users_actions = self.util.get_asset_system_users_with_actions(asset) - actions = system_users_actions.get(system_user) - return {"actions": actions} - - -class ValidateUserAssetPermissionApi(UserAssetPermissionMixin, APIView): - permission_classes = (IsOrgAdminOrAppUser,) - - def get_obj(self): - user_id = self.request.query_params.get('user_id', '') - user = get_object_or_404(User, id=user_id) - return user - - def get(self, request, *args, **kwargs): - asset_id = request.query_params.get('asset_id', '') - system_id = request.query_params.get('system_user_id', '') - action_name = request.query_params.get('action_name', '') - - try: - asset_id = uuid.UUID(asset_id) - system_id = uuid.UUID(system_id) - except ValueError: - return Response({'msg': False}, status=403) - - asset = get_object_or_404(Asset, id=asset_id) - system_user = get_object_or_404(SystemUser, id=system_id) - - system_users_actions = self.util.get_asset_system_users_with_actions(asset) - actions = system_users_actions.get(system_user) - if action_name in Action.value_to_choices(actions): - return Response({'msg': True}, status=200) - return Response({'msg': False}, status=403) - - -class RefreshAssetPermissionCacheApi(RetrieveAPIView): - permission_classes = (IsOrgAdmin,) - - def retrieve(self, request, *args, **kwargs): - AssetPermissionUtilV2.expire_all_user_tree_cache() - return Response({'msg': True}, status=200) - - -class UserGrantedAssetSystemUsersApi(UserAssetPermissionMixin, ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.AssetSystemUserSerializer - only_fields = serializers.AssetSystemUserSerializer.Meta.only_fields - - def get_queryset(self): - asset_id = self.kwargs.get('asset_id') - asset = get_object_or_404(Asset, id=asset_id) - system_users_with_actions = self.util.get_asset_system_users_with_actions(asset) - system_users = [] - for system_user, actions in system_users_with_actions.items(): - system_user.actions = actions - system_users.append(system_user) - system_users.sort(key=lambda x: x.priority) - return system_users diff --git a/apps/perms/api/user_permission/__init__.py b/apps/perms/api/user_permission/__init__.py new file mode 100644 index 000000000..590235cc6 --- /dev/null +++ b/apps/perms/api/user_permission/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# +from .common import * +from .user_permission_nodes import * +from .user_permission_assets import * +from .user_permission_nodes_with_assets import * diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py new file mode 100644 index 000000000..6a2f8adf7 --- /dev/null +++ b/apps/perms/api/user_permission/common.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +import uuid + +from django.shortcuts import get_object_or_404 +from rest_framework.views import APIView, Response +from rest_framework.generics import ( + ListAPIView, get_object_or_404, RetrieveAPIView +) + +from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin +from common.utils import get_logger +from ...utils import ( + AssetPermissionUtilV2 +) +from ...hands import User, Asset, SystemUser +from ... import serializers +from ...models import Action +from .mixin import UserAssetPermissionMixin + +logger = get_logger(__name__) + +__all__ = [ + 'RefreshAssetPermissionCacheApi', + 'UserGrantedAssetSystemUsersApi', + 'ValidateUserAssetPermissionApi', + 'GetUserAssetPermissionActionsApi', +] + + +class GetUserAssetPermissionActionsApi(UserAssetPermissionMixin, + RetrieveAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.ActionsSerializer + + def get_obj(self): + user_id = self.request.query_params.get('user_id', '') + user = get_object_or_404(User, id=user_id) + return user + + def get_object(self): + asset_id = self.request.query_params.get('asset_id', '') + system_id = self.request.query_params.get('system_user_id', '') + + try: + asset_id = uuid.UUID(asset_id) + system_id = uuid.UUID(system_id) + except ValueError: + return Response({'msg': False}, status=403) + + asset = get_object_or_404(Asset, id=asset_id) + system_user = get_object_or_404(SystemUser, id=system_id) + + system_users_actions = self.util.get_asset_system_users_with_actions( + asset) + actions = system_users_actions.get(system_user) + return {"actions": actions} + + +class ValidateUserAssetPermissionApi(UserAssetPermissionMixin, APIView): + permission_classes = (IsOrgAdminOrAppUser,) + + def get_obj(self): + user_id = self.request.query_params.get('user_id', '') + user = get_object_or_404(User, id=user_id) + return user + + def get(self, request, *args, **kwargs): + asset_id = request.query_params.get('asset_id', '') + system_id = request.query_params.get('system_user_id', '') + action_name = request.query_params.get('action_name', '') + + try: + asset_id = uuid.UUID(asset_id) + system_id = uuid.UUID(system_id) + except ValueError: + return Response({'msg': False}, status=403) + + asset = get_object_or_404(Asset, id=asset_id) + system_user = get_object_or_404(SystemUser, id=system_id) + + system_users_actions = self.util.get_asset_system_users_with_actions( + asset) + actions = system_users_actions.get(system_user) + if action_name in Action.value_to_choices(actions): + return Response({'msg': True}, status=200) + return Response({'msg': False}, status=403) + + +class RefreshAssetPermissionCacheApi(RetrieveAPIView): + permission_classes = (IsOrgAdmin,) + + def retrieve(self, request, *args, **kwargs): + AssetPermissionUtilV2.expire_all_user_tree_cache() + return Response({'msg': True}, status=200) + + +class UserGrantedAssetSystemUsersApi(UserAssetPermissionMixin, ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.AssetSystemUserSerializer + only_fields = serializers.AssetSystemUserSerializer.Meta.only_fields + + def get_queryset(self): + asset_id = self.kwargs.get('asset_id') + asset = get_object_or_404(Asset, id=asset_id) + system_users_with_actions = self.util.get_asset_system_users_with_actions( + asset) + system_users = [] + for system_user, actions in system_users_with_actions.items(): + system_user.actions = actions + system_users.append(system_user) + system_users.sort(key=lambda x: x.priority) + return system_users diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py new file mode 100644 index 000000000..3fb1e99ad --- /dev/null +++ b/apps/perms/api/user_permission/mixin.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +from ..mixin import UserPermissionMixin +from ...utils import AssetPermissionUtilV2, ParserNode +from ...hands import Node +from common.tree import TreeNodeSerializer + + +class UserAssetPermissionMixin(UserPermissionMixin): + util = None + tree = None + + def initial(self, *args, **kwargs): + super().initial(*args, *kwargs) + cache_policy = self.request.query_params.get('cache_policy', '0') + system_user_id = self.request.query_params.get("system_user") + self.util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) + if system_user_id: + self.util.filter_permissions(system_users=system_user_id) + self.tree = self.util.get_user_tree() + + +class UserNodeTreeMixin: + serializer_class = TreeNodeSerializer + nodes_only_fields = ParserNode.nodes_only_fields + + def parse_nodes_to_queryset(self, nodes): + nodes = nodes.only(*self.nodes_only_fields) + _queryset = [] + + for node in nodes: + assets_amount = self.tree.assets_amount(node.key) + if assets_amount == 0 and node.key != Node.empty_key: + continue + node._assets_amount = assets_amount + data = ParserNode.parse_node_to_tree_node(node) + _queryset.append(data) + return _queryset + + def get_serializer_queryset(self, queryset): + queryset = self.parse_nodes_to_queryset(queryset) + return queryset + + def get_serializer(self, queryset, many=True, **kwargs): + queryset = self.get_serializer_queryset(queryset) + queryset.sort() + return super().get_serializer(queryset, many=many, **kwargs) + + +class UserAssetTreeMixin: + serializer_class = TreeNodeSerializer + nodes_only_fields = ParserNode.assets_only_fields + + @staticmethod + def parse_assets_to_queryset(assets, node): + _queryset = [] + for asset in assets: + data = ParserNode.parse_asset_to_tree_node(node, asset) + _queryset.append(data) + return _queryset + + def get_serializer_queryset(self, queryset): + queryset = queryset.only(*self.nodes_only_fields) + _queryset = self.parse_assets_to_queryset(queryset, None) + return _queryset + + def get_serializer(self, queryset, many=True, **kwargs): + queryset = self.get_serializer_queryset(queryset) + queryset.sort() + return super().get_serializer(queryset, many=many, **kwargs) diff --git a/apps/perms/api/user_permission/user_permission_assets.py b/apps/perms/api/user_permission/user_permission_assets.py new file mode 100644 index 000000000..c97f59cce --- /dev/null +++ b/apps/perms/api/user_permission/user_permission_assets.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# + +from django.shortcuts import get_object_or_404 +from rest_framework.generics import ( + ListAPIView, get_object_or_404 +) + +from common.permissions import IsOrgAdminOrAppUser +from common.utils import get_logger +from ...hands import Node +from ... import serializers +from .mixin import UserAssetPermissionMixin, UserAssetTreeMixin + + +logger = get_logger(__name__) + +__all__ = [ + 'UserGrantedAssetsApi', + 'UserGrantedAssetsAsTreeApi', + 'UserGrantedNodeAssetsApi', +] + + +class UserGrantedAssetsApi(UserAssetPermissionMixin, ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.AssetGrantedSerializer + only_fields = serializers.AssetGrantedSerializer.Meta.only_fields + filter_fields = ['hostname', 'ip', 'id', 'comment'] + search_fields = ['hostname', 'ip', 'comment'] + + def filter_by_nodes(self, queryset): + node_id = self.request.query_params.get("node") + if not node_id: + return queryset + node = get_object_or_404(Node, pk=node_id) + query_all = self.request.query_params.get("all", "0") in ["1", "true"] + if query_all: + pattern = '^{0}$|^{0}:'.format(node.key) + queryset = queryset.filter(nodes__key__regex=pattern).distinct() + else: + queryset = queryset.filter(nodes=node) + return queryset + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.filter_by_nodes(queryset) + return queryset + + def get_queryset(self): + queryset = self.util.get_assets().only(*self.only_fields) + return queryset + + +class UserGrantedAssetsAsTreeApi(UserAssetTreeMixin, UserGrantedAssetsApi): + pass + + +class UserGrantedNodeAssetsApi(UserGrantedAssetsApi): + def get_queryset(self): + node_id = self.kwargs.get("node_id") + node = get_object_or_404(Node, pk=node_id) + deep = self.request.query_params.get("all", "0") == "1" + queryset = self.util.get_nodes_assets(node, deep=deep)\ + .only(*self.only_fields) + return queryset diff --git a/apps/perms/api/user_permission/user_permission_nodes.py b/apps/perms/api/user_permission/user_permission_nodes.py new file mode 100644 index 000000000..311f2a048 --- /dev/null +++ b/apps/perms/api/user_permission/user_permission_nodes.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# + +from django.shortcuts import get_object_or_404 +from rest_framework.generics import ( + ListAPIView, get_object_or_404 +) + +from common.permissions import IsOrgAdminOrAppUser +from common.utils import get_logger +from ...hands import Node, NodeSerializer +from ... import serializers +from .mixin import UserNodeTreeMixin, UserAssetPermissionMixin + + +logger = get_logger(__name__) + +__all__ = [ + 'UserGrantedNodesApi', + 'UserGrantedNodesAsTreeApi', + 'UserGrantedNodeChildrenApi', + 'UserGrantedNodeChildrenAsTreeApi', +] + + +class UserGrantedNodesApi(UserAssetPermissionMixin, ListAPIView): + """ + 查询用户授权的所有节点的API + """ + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.NodeGrantedSerializer + nodes_only_fields = NodeSerializer.Meta.only_fields + + def get_serializer_context(self): + context = super().get_serializer_context() + if self.serializer_class == serializers.NodeGrantedSerializer: + context["tree"] = self.tree + return context + + def get_queryset(self): + node_keys = self.util.get_nodes() + queryset = Node.objects.filter(key__in=node_keys)\ + .only(*self.nodes_only_fields) + return queryset + + +class UserGrantedNodesAsTreeApi(UserNodeTreeMixin, UserGrantedNodesApi): + pass + + +class UserGrantedNodeChildrenApi(UserGrantedNodesApi): + node = None + root_keys = None # 如果是第一次访问,则需要把二级节点添加进去,这个 roots_keys + + def get(self, request, *args, **kwargs): + key = self.request.query_params.get("key") + pk = self.request.query_params.get("id") + + node = None + if pk is not None: + node = get_object_or_404(Node, id=pk) + elif key is not None: + node = get_object_or_404(Node, key=key) + self.node = node + return super().get(request, *args, **kwargs) + + def get_queryset(self): + if self.node: + children = self.tree.children(self.node.key) + else: + children = self.tree.children(self.tree.root) + # 默认打开组织节点下的节点 + self.root_keys = [child.identifier for child in children] + for key in self.root_keys: + children.extend(self.tree.children(key)) + node_keys = [n.identifier for n in children] + queryset = Node.objects.filter(key__in=node_keys) + return queryset + + +class UserGrantedNodeChildrenAsTreeApi(UserNodeTreeMixin, UserGrantedNodeChildrenApi): + pass diff --git a/apps/perms/api/user_permission/user_permission_nodes_with_assets.py b/apps/perms/api/user_permission/user_permission_nodes_with_assets.py new file mode 100644 index 000000000..974705c16 --- /dev/null +++ b/apps/perms/api/user_permission/user_permission_nodes_with_assets.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +from common.utils import get_logger +from ...utils import ParserNode +from .mixin import UserAssetTreeMixin +from ...hands import Node +from .user_permission_nodes import UserGrantedNodesAsTreeApi +from .user_permission_nodes import UserGrantedNodeChildrenAsTreeApi + + +logger = get_logger(__name__) + +__all__ = [ + 'UserGrantedNodesAsTreeApi', + 'UserGrantedNodesWithAssetsAsTreeApi', + 'UserGrantedNodeChildrenAsTreeApi', + 'UserGrantedNodeChildrenWithAssetsAsTreeApi', +] + + +class UserGrantedNodesWithAssetsAsTreeApi(UserGrantedNodesAsTreeApi): + assets_only_fields = ParserNode.assets_only_fields + + def get_serializer_queryset(self, queryset): + _queryset = super().get_serializer_queryset(queryset) + _all_assets = self.util.get_assets().only(*self.assets_only_fields) + _all_assets_map = {a.id: a for a in _all_assets} + for node in queryset: + assets_ids = self.tree.assets(node.key) + assets = [_all_assets_map[_id] for _id in assets_ids if _id in _all_assets_map] + _queryset.extend( + UserAssetTreeMixin.parse_assets_to_queryset(assets, node) + ) + return _queryset + + +class UserGrantedNodeChildrenWithAssetsAsTreeApi(UserGrantedNodeChildrenAsTreeApi): + nodes_only_fields = ParserNode.nodes_only_fields + assets_only_fields = ParserNode.assets_only_fields + + def get_serializer_queryset(self, queryset): + _queryset = super().get_serializer_queryset(queryset) + nodes = [] + if self.node: + nodes.append(self.node) + elif self.root_keys: + nodes = Node.objects.filter(key__in=self.root_keys) + + for node in nodes: + assets = self.util.get_nodes_assets(node).only( + *self.assets_only_fields + ) + _queryset.extend( + UserAssetTreeMixin.parse_assets_to_queryset(assets, node) + ) + return _queryset diff --git a/apps/perms/templates/perms/asset_permission_asset.html b/apps/perms/templates/perms/asset_permission_asset.html index 093ab39a8..8e9223ec3 100644 --- a/apps/perms/templates/perms/asset_permission_asset.html +++ b/apps/perms/templates/perms/asset_permission_asset.html @@ -75,7 +75,7 @@
- @@ -207,7 +207,7 @@ $(document).ready(function () { var nodeListUrl = "{% url 'api-assets:node-list' %}"; nodesSelect2Init(".nodes-select2", nodeListUrl); - $("#asset_select2").parent().find(".select2-selection").on('click', function (e) { + $("#id_assets").parent().find(".select2-selection").on('click', function (e) { if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){ e.preventDefault(); e.stopPropagation(); @@ -216,7 +216,7 @@ $(document).ready(function () { }) }) .on('click', '.btn-add-assets', function () { - var assets_selected = $("#asset_select2 option:selected").map(function () { + var assets_selected = $("#id_assets option:selected").map(function () { return $(this).attr('value'); }).get(); if (assets_selected.length === 0) { diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 8e9f4a6f3..bb0ede53d 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -4,6 +4,8 @@ import pickle import threading from collections import defaultdict from functools import reduce +from hashlib import md5 +import json from django.core.cache import cache from django.db.models import Q @@ -60,29 +62,33 @@ def get_system_user_permissions(system_user): class AssetPermissionUtilCacheMixin: - user_tree_cache_key = 'USER_PERM_TREE_{}' + user_tree_cache_key = 'USER_PERM_TREE_{}_{}' user_tree_cache_ttl = settings.ASSETS_PERM_CACHE_TIME user_tree_cache_enable = settings.ASSETS_PERM_CACHE_ENABLE cache_policy = '0' obj_id = '' + _filter_id = 'None' + + @property + def cache_key(self): + return self.user_tree_cache_key.format(self.obj_id, self._filter_id) def expire_user_tree_cache(self): - key = self.user_tree_cache_key.format(self.obj_id) - cache.delete(key) + cache.delete(self.cache_key) @classmethod def expire_all_user_tree_cache(cls): - key = cls.user_tree_cache_key.format('*') + key = cls.user_tree_cache_key.format('*', '*') + key = key.split('_')[:-1] + key = '_'.join(key) cache.delete_pattern(key) def set_user_tree_to_cache(self, user_tree): data = pickle.dumps(user_tree) - key = self.user_tree_cache_key.format(self.obj_id) - cache.set(key, data, self.user_tree_cache_ttl) + cache.set(self.cache_key, data, self.user_tree_cache_ttl) def get_user_tree_from_cache(self): - key = self.user_tree_cache_key.format(self.obj_id) - data = cache.get(key) + data = cache.get(self.cache_key) if not data: return None user_tree = pickle.loads(data) @@ -129,6 +135,7 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): self._filter_id = 'None' # 当通过filter更改 permission是标记 self.change_org_if_need() self._user_tree = None + self._user_tree_filter_id = 'None' self.full_tree = Node.tree() self.mutex = threading.Lock() @@ -148,7 +155,9 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): @timeit def filter_permissions(self, **filters): + filters_json = json.dumps(filters, sort_keys=True) self._permissions = self.permissions.filter(**filters) + self._filter_id = md5(filters_json.encode()).hexdigest() @property def user_tree(self): @@ -282,15 +291,25 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): parent=user_tree.root, ) + def set_user_tree_to_local(self, user_tree): + self._user_tree = user_tree + self._user_tree_filter_id = self._filter_id + + def get_user_tree_from_local(self): + if self._user_tree and self._user_tree_filter_id == self._filter_id: + return self._user_tree + return None + @timeit def get_user_tree(self): # 使用锁,保证多次获取tree的时候顺序执行,可以使用缓存 with self.mutex: - if self._user_tree: - return self._user_tree + user_tree = self.get_user_tree_from_local() + if user_tree: + return user_tree user_tree = self.get_user_tree_from_cache_if_need() if user_tree: - self._user_tree = user_tree + self.set_user_tree_to_local(user_tree) return user_tree user_tree = TreeService() full_tree_root = self.full_tree.root_node() @@ -303,7 +322,7 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): self.parse_user_tree_to_full_tree(user_tree) self.add_empty_node_if_need(user_tree) self.set_user_tree_to_cache_if_need(user_tree) - self._user_tree = user_tree + self.set_user_tree_to_local(user_tree) return user_tree # Todo: 是否可以获取多个资产的系统用户 diff --git a/apps/settings/signals_handler.py b/apps/settings/signals_handler.py index cb7603ec7..c131cc214 100644 --- a/apps/settings/signals_handler.py +++ b/apps/settings/signals_handler.py @@ -22,7 +22,7 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): instance.refresh_setting() -@receiver(django_ready, dispatch_uid="my_unique_identifier") +@receiver(django_ready) def monkey_patch_settings(sender, **kwargs): logger.debug("Monkey patch settings") cache_key_prefix = '_SETTING_'