diff --git a/README.md b/README.md index 595677321..76465a36e 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,26 @@ -# Jumpserver 多云环境下更好用的堡垒机 +# JumpServer 多云环境下更好用的堡垒机 [](https://www.python.org/) [](https://www.djangoproject.com/) [](https://www.ansible.com/) [](http://www.paramiko.org/) -Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 机制的运维安全审计系统。 +JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 机制的运维安全审计系统。 -Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。 +JumpServer 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。 -Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。 +JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。 改变世界,从一点点开始。 -注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 Jumpserver 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。 +注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。 ## 核心功能列表
身份认证 Authentication |
- 登录认证 | +登录认证 | 资源统一登录与认证 |
OpenID 认证(实现单点登录) | |||
CAS 认证 (实现单点登录) | +|||
MFA认证 | MFA 二次认证(Google Authenticator) | @@ -177,17 +180,10 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向 ## 演示视频和截屏 -我们提供了演示视频和系统截图可以让你快速了解 Jumpserver: +我们提供了演示视频和系统截图可以让你快速了解 JumpServer: - [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4) -- [系统截图](http://docs.jumpserver.org/zh/docs/snapshot.html) - -## SDK - -我们编写了一些SDK,供您的其它系统快速和 Jumpserver API 交互: - -- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver 其它组件使用这个 SDK 完成交互 -- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的 Java 版本的 SDK +- [系统截图](http://docs.JumpServer.org/zh/docs/snapshot.html) ## License & Copyright diff --git a/apps/applications/hands.py b/apps/applications/hands.py index 7c83e1332..ee13e589e 100644 --- a/apps/applications/hands.py +++ b/apps/applications/hands.py @@ -6,7 +6,7 @@ Other module of this app shouldn't connect with other app. - :copyright: (c) 2014-2018 by Jumpserver Team. + :copyright: (c) 2014-2018 by JumpServer Team. :license: GPL v2, see LICENSE for more details. """ diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 0b63522ea..6eb8ebeaf 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -23,8 +23,8 @@ from ..filters import AssetByNodeFilterBackend, LabelFilterBackend logger = get_logger(__file__) __all__ = [ 'AssetViewSet', 'AssetPlatformRetrieveApi', - 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', - 'AssetGatewayApi', 'AssetPlatformViewSet', + 'AssetGatewayListApi', 'AssetPlatformViewSet', + 'AssetTaskCreateApi', ] @@ -36,7 +36,10 @@ class AssetViewSet(OrgBulkModelViewSet): filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id") search_fields = ("hostname", "ip") ordering_fields = ("hostname", "ip", "port", "cpu_cores") - serializer_class = serializers.AssetSerializer + serializer_classes = { + 'default': serializers.AssetSerializer, + 'display': serializers.AssetDisplaySerializer, + } permission_classes = (IsOrgAdminOrAppUser,) extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] @@ -80,53 +83,40 @@ class AssetPlatformViewSet(ModelViewSet): self.permission_denied( request, message={"detail": "Internal platform"} ) - return super().check_object_permissions(request, obj) -class AssetRefreshHardwareApi(generics.RetrieveAPIView): - """ - Refresh asset hardware info - """ +class AssetTaskCreateApi(generics.CreateAPIView): model = Asset - serializer_class = serializers.AssetSerializer + serializer_class = serializers.AssetTaskSerializer permission_classes = (IsOrgAdmin,) - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - task = update_asset_hardware_info_manual.delay(asset) - return Response({"task": task.id}) + def get_object(self): + pk = self.kwargs.get("pk") + instance = get_object_or_404(Asset, pk=pk) + return instance + + def perform_create(self, serializer): + asset = self.get_object() + action = serializer.validated_data["action"] + if action == "refresh": + task = update_asset_hardware_info_manual.delay(asset) + else: + task = test_asset_connectivity_manual.delay(asset) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) -class AssetAdminUserTestApi(generics.RetrieveAPIView): - """ - Test asset admin user assets_connectivity - """ - model = Asset - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer - - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - task = test_asset_connectivity_manual.delay(asset) - return Response({"task": task.id}) - - -class AssetGatewayApi(generics.RetrieveAPIView): +class AssetGatewayListApi(generics.ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.GatewayWithAuthSerializer model = Asset - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') + def get_queryset(self): + asset_id = self.kwargs.get('pk') asset = get_object_or_404(Asset, pk=asset_id) - - if asset.domain and \ - asset.domain.gateways.filter(protocol='ssh').exists(): - gateway = random.choice(asset.domain.gateways.filter(protocol='ssh')) - serializer = serializers.GatewayWithAuthSerializer(instance=gateway) - return Response(serializer.data) - else: - return Response({"msg": "Not have gateway"}, status=404) + if not asset.domain: + return [] + queryset = asset.domain.gateways.filter(protocol='ssh') + return queryset diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index a3b9d8906..c0d834737 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -1,26 +1,23 @@ # -*- coding: utf-8 -*- # - -from rest_framework.response import Response -from rest_framework import generics -from rest_framework import filters -from rest_framework_bulk import BulkModelViewSet -from django.shortcuts import get_object_or_404 -from django.http import Http404 from django.conf import settings +from rest_framework.response import Response +from rest_framework import generics, filters +from rest_framework_bulk import BulkModelViewSet from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger from common.mixins import CommonApiMixin from ..backends import AssetUserManager -from ..models import Asset, Node, SystemUser, AdminUser +from ..models import Asset, Node, SystemUser from .. import serializers -from ..tasks import test_asset_users_connectivity_manual +from ..tasks import ( + test_asset_users_connectivity_manual, push_system_user_a_asset_manual +) __all__ = [ - 'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi', - 'AssetUserExportViewSet', + 'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI', ] @@ -34,10 +31,17 @@ class AssetUserFilterBackend(filters.BaseFilterBackend): value = request.GET.get(field) if not value: continue - if field in ("node_id", "system_user_id", "admin_user_id"): + if field == "node_id": + value = get_object_or_none(Node, pk=value) + kwargs["node"] = value continue + elif field == "asset_id": + field = "asset" kwargs[field] = value - return queryset.filter(**kwargs) + if kwargs: + queryset = queryset.filter(**kwargs) + logger.debug("Filter {}".format(kwargs)) + return queryset class AssetUserSearchBackend(filters.BaseFilterBackend): @@ -45,72 +49,63 @@ class AssetUserSearchBackend(filters.BaseFilterBackend): value = request.GET.get('search') if not value: return queryset - _queryset = AssetUserManager.none() - for field in view.search_fields: - if field in ("node_id", "system_user_id", "admin_user_id"): - continue - _queryset |= queryset.filter(**{field: value}) - return _queryset.distinct() + queryset = queryset.search(value) + return queryset + + +class AssetUserLatestFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + latest = request.GET.get('latest') == '1' + if latest: + queryset = queryset.distinct() + return queryset class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): - serializer_class = serializers.AssetUserSerializer + serializer_classes = { + 'default': serializers.AssetUserWriteSerializer, + 'list': serializers.AssetUserReadSerializer, + 'retrieve': serializers.AssetUserReadSerializer, + } permission_classes = [IsOrgAdminOrAppUser] - http_method_names = ['get', 'post'] filter_fields = [ - "id", "ip", "hostname", "username", "asset_id", "node_id", - "system_user_id", "admin_user_id" + "id", "ip", "hostname", "username", + "asset_id", "node_id", + "prefer", "prefer_id", ] - search_fields = filter_fields - filter_backends = ( - filters.OrderingFilter, + search_fields = ["ip", "hostname", "username"] + filter_backends = [ AssetUserFilterBackend, AssetUserSearchBackend, - ) + AssetUserLatestFilterBackend, + ] def allow_bulk_destroy(self, qs, filtered): return False - def get_queryset(self): - # 尽可能先返回更少的数据 - username = self.request.GET.get('username') - asset_id = self.request.GET.get('asset_id') - node_id = self.request.GET.get('node_id') - admin_user_id = self.request.GET.get("admin_user_id") - system_user_id = self.request.GET.get("system_user_id") + def get_object(self): + pk = self.kwargs.get("pk") + queryset = self.get_queryset() + obj = queryset.get(id=pk) + return obj - kwargs = {} - assets = None + def get_exception_handler(self): + def handler(e, context): + return Response({"error": str(e)}, status=400) + return handler + def perform_destroy(self, instance): manager = AssetUserManager() - if system_user_id: - system_user = get_object_or_404(SystemUser, id=system_user_id) - assets = system_user.get_all_assets() - username = system_user.username - elif admin_user_id: - admin_user = get_object_or_404(AdminUser, id=admin_user_id) - assets = admin_user.assets.all() - username = admin_user.username - manager.prefer('admin_user') + manager.delete(instance) - if asset_id: - asset = get_object_or_404(Asset, id=asset_id) - assets = [asset] - elif node_id: - node = get_object_or_404(Node, id=node_id) - assets = node.get_all_assets() - - if username: - kwargs['username'] = username - if assets is not None: - kwargs['assets'] = assets - - queryset = manager.filter(**kwargs) + def get_queryset(self): + manager = AssetUserManager() + queryset = manager.all() return queryset -class AssetUserExportViewSet(AssetUserViewSet): - serializer_class = serializers.AssetUserExportSerializer - http_method_names = ['get'] +class AssetUserAuthInfoViewSet(AssetUserViewSet): + serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer} + http_method_names = ['get', 'post'] permission_classes = [IsOrgAdminOrAppUser] def get_permissions(self): @@ -119,66 +114,31 @@ class AssetUserExportViewSet(AssetUserViewSet): return super().get_permissions() -class AssetUserAuthInfoApi(generics.RetrieveAPIView): - serializer_class = serializers.AssetUserAuthInfoSerializer - permission_classes = [IsOrgAdminOrAppUser] - - def get_permissions(self): - if settings.SECURITY_VIEW_AUTH_NEED_MFA: - self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] - return super().get_permissions() - - def get_object(self): - query_params = self.request.query_params - username = query_params.get('username') - asset_id = query_params.get('asset_id') - prefer = query_params.get("prefer") - asset = get_object_or_none(Asset, pk=asset_id) - try: - manger = AssetUserManager() - instance = manger.get(username, asset, prefer=prefer) - except Exception as e: - raise Http404("Not found") - else: - return instance - - -class AssetUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Test asset users connective - """ +class AssetUserTaskCreateAPI(generics.CreateAPIView): permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.TaskIDSerializer + serializer_class = serializers.AssetUserTaskSerializer + filter_backends = AssetUserViewSet.filter_backends + filter_fields = AssetUserViewSet.filter_fields def get_asset_users(self): - username = self.request.GET.get('username') - asset_id = self.request.GET.get('asset_id') - prefer = self.request.GET.get("prefer") - asset = get_object_or_none(Asset, pk=asset_id) manager = AssetUserManager() - asset_users = manager.filter(username=username, assets=[asset], prefer=prefer) - return asset_users + queryset = manager.all() + for cls in self.filter_backends: + queryset = cls().filter_queryset(self.request, queryset, self) + return list(queryset) - def retrieve(self, request, *args, **kwargs): + def perform_create(self, serializer): asset_users = self.get_asset_users() - prefer = self.request.GET.get("prefer") - kwargs = {} - if prefer == "admin_user": - kwargs["run_as_admin"] = True - task = test_asset_users_connectivity_manual.delay(asset_users, **kwargs) - return Response({"task": task.id}) + # action = serializer.validated_data["action"] + # only this + # if action == "test": + task = test_asset_users_connectivity_manual.delay(asset_users) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) + return task - -class AssetUserPushApi(generics.CreateAPIView): - """ - Test asset users connective - """ - serializer_class = serializers.AssetUserPushSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - asset = serializer.validated_data["asset"] - username = serializer.validated_data["username"] - pass + def get_exception_handler(self): + def handler(e, context): + return Response({"error": str(e)}, status=400) + return handler diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 24661bad6..2d959e9ea 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,24 +1,11 @@ # ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +from collections import namedtuple from rest_framework import status from rest_framework.serializers import ValidationError -from rest_framework.views import APIView from rest_framework.response import Response from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, Http404 from common.utils import get_logger, get_object_or_none from common.tree import TreeNodeSerializer @@ -27,7 +14,8 @@ from orgs.mixins import generics from ..hands import IsOrgAdmin from ..models import Node from ..tasks import ( - update_assets_hardware_info_util, test_asset_connectivity_util + update_node_assets_hardware_info_manual, + test_node_assets_connectivity_manual, ) from .. import serializers @@ -36,9 +24,9 @@ logger = get_logger(__file__) __all__ = [ 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', - 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', - 'TestNodeConnectiveApi', 'NodeListAsTreeApi', - 'NodeChildrenAsTreeApi', 'RefreshNodesCacheApi', + 'NodeAddChildrenApi', 'NodeListAsTreeApi', + 'NodeChildrenAsTreeApi', + 'NodeTaskCreateApi', ] @@ -64,9 +52,9 @@ class NodeViewSet(OrgModelViewSet): def destroy(self, request, *args, **kwargs): node = self.get_object() - if node.has_children_or_contains_assets(): - msg = _("Deletion failed and the node contains children or assets") - return Response(data={'msg': msg}, status=status.HTTP_403_FORBIDDEN) + if node.has_children_or_has_assets(): + error = _("Deletion failed and the node contains children or assets") + return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) return super().destroy(request, *args, **kwargs) @@ -261,41 +249,41 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView): asset.nodes.set([instance]) -class RefreshNodeHardwareInfoApi(APIView): +class NodeTaskCreateApi(generics.CreateAPIView): model = Node + serializer_class = serializers.NodeTaskSerializer permission_classes = (IsOrgAdmin,) - def get(self, request, *args, **kwargs): - node_id = kwargs.get('pk') - node = get_object_or_404(self.model, id=node_id) - assets = node.get_all_assets() - # task_name = _("更新节点资产硬件信息: {}".format(node.name)) - task_name = _("Update node asset hardware information: {}").format(node.name) - task = update_assets_hardware_info_util.delay(assets, task_name=task_name) - return Response({"task": task.id}) + def get_object(self): + node_id = self.kwargs.get('pk') + node = get_object_or_none(self.model, id=node_id) + return node + @staticmethod + def set_serializer_data(s, task): + data = getattr(s, '_data', {}) + data["task"] = task.id + setattr(s, '_data', data) -class TestNodeConnectiveApi(APIView): - permission_classes = (IsOrgAdmin,) - model = Node - - def get(self, request, *args, **kwargs): - node_id = kwargs.get('pk') - node = get_object_or_404(self.model, id=node_id) - assets = node.get_all_assets() - # task_name = _("测试节点下资产是否可连接: {}".format(node.name)) - task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) - task = test_asset_connectivity_util.delay(assets, task_name=task_name) - return Response({"task": task.id}) - - -class RefreshNodesCacheApi(APIView): - permission_classes = (IsOrgAdmin,) - - def get(self, request, *args, **kwargs): + @staticmethod + def refresh_nodes_cache(): Node.refresh_nodes() - return Response("Ok") + Task = namedtuple('Task', ['id']) + task = Task(id="0") + return task + + def perform_create(self, serializer): + action = serializer.validated_data["action"] + node = self.get_object() + if action == "refresh_cache" and node is None: + task = self.refresh_nodes_cache() + self.set_serializer_data(serializer, task) + return + if node is None: + raise Http404() + if action == "refresh": + task = update_node_assets_hardware_info_manual.delay(node) + else: + task = test_node_assets_connectivity_manual.delay(node) + self.set_serializer_data(serializer, task) - def delete(self, *args, **kwargs): - self.get(*args, **kwargs) - return Response(status=204) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index e3dddd6f5..dc134ee97 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -1,42 +1,25 @@ # ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from django.shortcuts import get_object_or_404 from rest_framework.response import Response -from django.db.models import Count -from common.serializers import CeleryTaskSerializer from common.utils import get_logger from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics +from orgs.utils import tmp_to_org from ..models import SystemUser, Asset from .. import serializers +from ..serializers import SystemUserWithAuthInfoSerializer from ..tasks import ( push_system_user_to_assets_manual, test_system_user_connectivity_manual, - push_system_user_a_asset_manual, test_system_user_connectivity_a_asset, + push_system_user_a_asset_manual, ) logger = get_logger(__file__) __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', - 'SystemUserPushApi', 'SystemUserTestConnectiveApi', - 'SystemUserAssetsListView', 'SystemUserPushToAssetApi', - 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi', - + 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', ] @@ -48,13 +31,12 @@ class SystemUserViewSet(OrgBulkModelViewSet): filter_fields = ("name", "username") search_fields = filter_fields serializer_class = serializers.SystemUserSerializer + serializer_classes = { + 'default': serializers.SystemUserSerializer, + 'list': serializers.SystemUserListSerializer, + } permission_classes = (IsOrgAdminOrAppUser,) - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(_assets_amount=Count('assets')) - return queryset - class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ @@ -62,7 +44,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ model = SystemUser permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.SystemUserAuthSerializer + serializer_class = SystemUserWithAuthInfoSerializer def destroy(self, request, *args, **kwargs): instance = self.get_object() @@ -75,88 +57,61 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): Get system user with asset auth info """ model = SystemUser - permission_classes = (IsAppUser,) - serializer_class = serializers.SystemUserAuthSerializer + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = SystemUserWithAuthInfoSerializer + + def get_exception_handler(self): + def handler(e, context): + return Response({"error": str(e)}, status=400) + return handler def get_object(self): instance = super().get_object() - aid = self.kwargs.get('aid') - asset = get_object_or_404(Asset, pk=aid) - instance.load_specific_asset_auth(asset) - return instance + username = instance.username + if instance.username_same_with_user: + username = self.request.query_params.get("username") + asset_id = self.kwargs.get('aid') + asset = get_object_or_404(Asset, pk=asset_id) + + with tmp_to_org(asset.org_id): + instance.load_asset_special_auth(asset=asset, username=username) + return instance -class SystemUserPushApi(generics.RetrieveAPIView): - """ - Push system user to cluster assets api - """ - model = SystemUser +class SystemUserTaskApi(generics.CreateAPIView): permission_classes = (IsOrgAdmin,) - serializer_class = CeleryTaskSerializer + serializer_class = serializers.SystemUserTaskSerializer - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - nodes = system_user.nodes.all() - for node in nodes: - system_user.assets.add(*tuple(node.get_all_assets())) - task = push_system_user_to_assets_manual.delay(system_user) - return Response({"task": task.id}) + def do_push(self, system_user, asset=None): + if asset is None: + task = push_system_user_to_assets_manual.delay(system_user) + else: + username = self.request.query_params.get('username') + task = push_system_user_a_asset_manual.delay( + system_user, asset, username=username + ) + return task - -class SystemUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Push system user to cluster assets api - """ - model = SystemUser - permission_classes = (IsOrgAdmin,) - serializer_class = CeleryTaskSerializer - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() + @staticmethod + def do_test(system_user, asset=None): task = test_system_user_connectivity_manual.delay(system_user) - return Response({"task": task.id}) - - -class SystemUserAssetsListView(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetSimpleSerializer - filter_fields = ("hostname", "ip") - http_method_names = ['get'] - search_fields = filter_fields + return task def get_object(self): pk = self.kwargs.get('pk') return get_object_or_404(SystemUser, pk=pk) - def get_queryset(self): + def perform_create(self, serializer): + action = serializer.validated_data["action"] + asset = serializer.validated_data.get('asset') system_user = self.get_object() - return system_user.assets.all() - - -class SystemUserPushToAssetApi(generics.RetrieveAPIView): - model = SystemUser - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - asset_id = self.kwargs.get('aid') - asset = get_object_or_404(Asset, id=asset_id) - task = push_system_user_a_asset_manual.delay(system_user, asset) - return Response({"task": task.id}) - - -class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView): - model = SystemUser - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - asset_id = self.kwargs.get('aid') - asset = get_object_or_404(Asset, id=asset_id) - task = test_system_user_connectivity_a_asset.delay(system_user, asset) - return Response({"task": task.id}) + if action == 'push': + task = self.do_push(system_user, asset) + else: + task = self.do_test(system_user, asset) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) class SystemUserCommandFilterRuleListApi(generics.ListAPIView): diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py index de88cfe38..69605b74a 100644 --- a/apps/assets/api/system_user_relation.py +++ b/apps/assets/api/system_user_relation.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # +from collections import defaultdict from django.db.models import F, Value +from django.db.models.signals import m2m_changed from django.db.models.functions import Concat from common.permissions import IsOrgAdmin @@ -8,10 +10,13 @@ from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org from .. import models, serializers -__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet'] +__all__ = [ + 'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet', + 'SystemUserUserRelationViewSet', +] -class RelationMixin(OrgBulkModelViewSet): +class RelationMixin: def get_queryset(self): queryset = self.model.objects.all() org_id = current_org.org_id() @@ -23,8 +28,40 @@ class RelationMixin(OrgBulkModelViewSet): )) return queryset + def send_post_add_signal(self, instance): + if not isinstance(instance, list): + instance = [instance] -class SystemUserAssetRelationViewSet(RelationMixin): + system_users_objects_map = defaultdict(list) + model, object_field = self.get_objects_attr() + + for i in instance: + _id = getattr(i, object_field).id + system_users_objects_map[i.systemuser].append(_id) + + sender = self.get_sender() + for system_user, objects in system_users_objects_map.items(): + m2m_changed.send( + sender=sender, instance=system_user, action='post_add', + reverse=False, model=model, pk_set=objects + ) + + def get_sender(self): + return self.model + + def get_objects_attr(self): + return models.Asset, 'asset' + + def perform_create(self, serializer): + instance = serializer.save() + self.send_post_add_signal(instance) + + +class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet): + pass + + +class SystemUserAssetRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserAssetRelationSerializer model = models.SystemUser.assets.through permission_classes = (IsOrgAdmin,) @@ -36,6 +73,9 @@ class SystemUserAssetRelationViewSet(RelationMixin): "systemuser__name", "systemuser__username" ] + def get_objects_attr(self): + return models.Asset, 'asset' + def get_queryset(self): queryset = super().get_queryset() queryset = queryset.annotate( @@ -47,7 +87,7 @@ class SystemUserAssetRelationViewSet(RelationMixin): return queryset -class SystemUserNodeRelationViewSet(RelationMixin): +class SystemUserNodeRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserNodeRelationSerializer model = models.SystemUser.nodes.through permission_classes = (IsOrgAdmin,) @@ -58,8 +98,39 @@ class SystemUserNodeRelationViewSet(RelationMixin): "node__value", "systemuser__name", "systemuser_username" ] + def get_objects_attr(self): + return models.Node, 'node' + def get_queryset(self): queryset = super().get_queryset() queryset = queryset \ .annotate(node_key=F('node__key')) return queryset + + +class SystemUserUserRelationViewSet(BaseRelationViewSet): + serializer_class = serializers.SystemUserUserRelationSerializer + model = models.SystemUser.users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'user', 'systemuser', + ] + search_fields = [ + "user__username", "user__name", + "systemuser__name", "systemuser__username", + ] + + def get_objects_attr(self): + from users.models import User + return User, 'user' + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + user_display=Concat( + F('user__name'), Value('('), + F('user__username'), Value(')') + ) + ) + return queryset + diff --git a/apps/assets/backends/admin_user.py b/apps/assets/backends/admin_user.py deleted file mode 100644 index 8f3644b1b..000000000 --- a/apps/assets/backends/admin_user.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from ..models import AdminUser -from .asset_user import AssetUserBackend - - -class AdminUserBackend(AssetUserBackend): - model = AdminUser - backend = 'AdminUser' diff --git a/apps/assets/backends/asset_user.py b/apps/assets/backends/asset_user.py deleted file mode 100644 index e76baf14b..000000000 --- a/apps/assets/backends/asset_user.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# -from collections import defaultdict -from .base import BaseBackend - - -class AssetUserBackend(BaseBackend): - model = None - backend = "AssetUser" - - @classmethod - def filter_queryset_more(cls, queryset): - return queryset - - @classmethod - def filter(cls, username=None, assets=None, **kwargs): - queryset = cls.model.objects.all() - prefer_id = kwargs.get('prefer_id') - if prefer_id: - queryset = queryset.filter(id=prefer_id) - instances = cls.construct_authbook_objects(queryset, assets) - return instances - if username: - queryset = queryset.filter(username=username) - if assets: - queryset = queryset.filter(assets__in=assets).distinct() - - queryset = cls.filter_queryset_more(queryset) - instances = cls.construct_authbook_objects(queryset, assets) - return instances - - @classmethod - def construct_authbook_objects(cls, asset_users, assets): - instances = [] - assets_user_assets_map = defaultdict(set) - if isinstance(asset_users, list): - assets_user_assets_map = { - asset_user.id: asset_user.assets.values_list('id', flat=True) - for asset_user in asset_users - } - else: - assets_user_assets = asset_users.values_list('id', 'assets') - for i, asset_id in assets_user_assets: - assets_user_assets_map[i].add(asset_id) - - for asset_user in asset_users: - if not assets: - related_assets = asset_user.assets.all() - else: - assets_map = {a.id: a for a in assets} - related_assets = [ - assets_map.get(i) for i in assets_user_assets_map.get(asset_user.id) if i in assets_map - ] - for asset in related_assets: - instance = asset_user.construct_to_authbook(asset) - instance.backend = cls.backend - instances.append(instance) - return instances diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py index 9c9f7ca1e..d5ce1f903 100644 --- a/apps/assets/backends/base.py +++ b/apps/assets/backends/base.py @@ -1,94 +1,48 @@ # -*- coding: utf-8 -*- # -import uuid from abc import abstractmethod +from ..models import Asset + class BaseBackend: - @classmethod @abstractmethod - def filter(cls, username=None, assets=None, latest=True, prefer=None, prefer_id=None): - """ - :param username: 用户名 - :param assets:||
{% trans 'Username' %}: | -{{ system_user.username }} | + {% if system_user.username_same_with_user %} +{% trans 'Username same with user' %} | + {% else %} +{{ system_user.username }} | + {% endif %}
{% trans 'Login mode' %}: | @@ -95,7 +106,7 @@|||
{% trans 'Created by' %}: | -{{ asset_group.created_by }} | +{{ system_user.created_by }} | |
{% trans 'Comment' %}: | @@ -131,26 +142,6 @@|||
{% trans 'Push system user now' %}: | -- - - - | -||
{% trans 'Test assets connective' %}: | -- - - - | -
- {% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %} -
-- {% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %} -
-- {% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %} -
-- {% trans "Changes the world, starting with a little bit." %} -
+{% block content %} +{{ form.non_field_errors.as_text }}
{{ form.non_field_errors.as_text }}
-{% trans 'Captcha invalid' %}
- {% endif %} - -{{ form.errors.username.as_text }}
-{{ form.errors.password.as_text }}
-- Demo账号: admin 密码: admin -
- {% endif %} - - - - {% if AUTH_OPENID %} - -{% trans "More login options" %}
-{% trans 'Captcha invalid' %}
+ {% endif %} +{{ form.errors.username.as_text }}
{{ form.errors.password.as_text }}
++ Demo账号: admin 密码: admin +
+ {% endif %} + + -{% trans "More login options" %}
+- {% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %} -
-- {% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %} -
-- {% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %} -
-- {% trans "Changes the world, starting with a little bit." %} -
- -{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}
-{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}
-{{ form.otp_code.errors.as_text }}
- {% endif %} --
-{{ form.otp_code.errors.as_text }}
+ {% endif %} +
@@ -74,7 +74,7 @@
{% endblock %}
diff --git a/apps/ops/templates/ops/task_detail.html b/apps/ops/templates/ops/task_detail.html
index d2e46cc35..39696b3d9 100644
--- a/apps/ops/templates/ops/task_detail.html
+++ b/apps/ops/templates/ops/task_detail.html
@@ -21,10 +21,10 @@
{% trans 'Task versions' %}
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{% trans 'Last run' %}: | -{{ object.latest_history.date_start }} | +{{ object.latest_execution.date_start }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{% trans 'Time delta' %}: | -{{ object.latest_history.timedelta|floatformat}} s | +{{ object.latest_execution.timedelta|floatformat}} s | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{% trans 'Is finished' %}: |
- {% if object.latest_history.is_finished %}
+ {% if object.latest_execution.is_finished %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
@@ -91,7 +91,7 @@
{% trans 'Is success ' %}: |
- {% if object.latest_history.is_success %}
+ {% if object.latest_execution.is_success %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
@@ -121,7 +121,7 @@
|
|