Merge pull request #3796 from jumpserver/dev

Dev
pull/3882/head
BaiJiangJie 2020-03-19 14:26:23 +08:00 committed by GitHub
commit 1c54e5acd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
211 changed files with 6709 additions and 3973 deletions

View File

@ -1,26 +1,26 @@
# Jumpserver 多云环境下更好用的堡垒机 # JumpServer 多云环境下更好用的堡垒机
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/) [![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/) [![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Ansible](https://img.shields.io/badge/ansible-2.4.2.0-blue.svg?style=plastic)](https://www.ansible.com/) [![Ansible](https://img.shields.io/badge/ansible-2.4.2.0-blue.svg?style=plastic)](https://www.ansible.com/)
[![Paramiko](https://img.shields.io/badge/paramiko-2.4.1-green.svg?style=plastic)](http://www.paramiko.org/) [![Paramiko](https://img.shields.io/badge/paramiko-2.4.1-green.svg?style=plastic)](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 领域的的又一全新力作,欢迎关注和使用。
## 核心功能列表 ## 核心功能列表
<table> <table>
<tr> <tr>
<td rowspan="7">身份认证<br>Authentication</td> <td rowspan="7">身份认证<br>Authentication</td>
<td rowspan="4">登录认证</td> <td rowspan="5">登录认证</td>
<td>资源统一登录与认证</td> <td>资源统一登录与认证</td>
</tr> </tr>
<tr> <tr>
@ -32,6 +32,9 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向
<tr> <tr>
<td>OpenID 认证(实现单点登录)</td> <td>OpenID 认证(实现单点登录)</td>
</tr> </tr>
<tr>
<td>CAS 认证 (实现单点登录)</td>
</tr>
<tr> <tr>
<td rowspan="2">MFA认证</td> <td rowspan="2">MFA认证</td>
<td>MFA 二次认证Google Authenticator</td> <td>MFA 二次认证Google Authenticator</td>
@ -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) - [演示视频](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) - [系统截图](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
## License & Copyright ## License & Copyright

View File

@ -6,7 +6,7 @@
Other module of this app shouldn't connect with other app. 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. :license: GPL v2, see LICENSE for more details.
""" """

View File

@ -23,8 +23,8 @@ from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi', 'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', 'AssetGatewayListApi', 'AssetPlatformViewSet',
'AssetGatewayApi', 'AssetPlatformViewSet', 'AssetTaskCreateApi',
] ]
@ -36,7 +36,10 @@ class AssetViewSet(OrgBulkModelViewSet):
filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id") filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id")
search_fields = ("hostname", "ip") search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores") ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_class = serializers.AssetSerializer serializer_classes = {
'default': serializers.AssetSerializer,
'display': serializers.AssetDisplaySerializer,
}
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
@ -80,53 +83,40 @@ class AssetPlatformViewSet(ModelViewSet):
self.permission_denied( self.permission_denied(
request, message={"detail": "Internal platform"} request, message={"detail": "Internal platform"}
) )
return super().check_object_permissions(request, obj) return super().check_object_permissions(request, obj)
class AssetRefreshHardwareApi(generics.RetrieveAPIView): class AssetTaskCreateApi(generics.CreateAPIView):
"""
Refresh asset hardware info
"""
model = Asset model = Asset
serializer_class = serializers.AssetSerializer serializer_class = serializers.AssetTaskSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs): def get_object(self):
asset_id = kwargs.get('pk') pk = self.kwargs.get("pk")
asset = get_object_or_404(Asset, pk=asset_id) 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) task = update_asset_hardware_info_manual.delay(asset)
return Response({"task": task.id}) else:
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) task = test_asset_connectivity_manual.delay(asset)
return Response({"task": task.id}) data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
class AssetGatewayApi(generics.RetrieveAPIView): class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer serializer_class = serializers.GatewayWithAuthSerializer
model = Asset model = Asset
def retrieve(self, request, *args, **kwargs): def get_queryset(self):
asset_id = kwargs.get('pk') asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
if not asset.domain:
if asset.domain and \ return []
asset.domain.gateways.filter(protocol='ssh').exists(): queryset = asset.domain.gateways.filter(protocol='ssh')
gateway = random.choice(asset.domain.gateways.filter(protocol='ssh')) return queryset
serializer = serializers.GatewayWithAuthSerializer(instance=gateway)
return Response(serializer.data)
else:
return Response({"msg": "Not have gateway"}, status=404)

View File

@ -1,26 +1,23 @@
# -*- coding: utf-8 -*- # -*- 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 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.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
from common.utils import get_object_or_none, get_logger from common.utils import get_object_or_none, get_logger
from common.mixins import CommonApiMixin from common.mixins import CommonApiMixin
from ..backends import AssetUserManager from ..backends import AssetUserManager
from ..models import Asset, Node, SystemUser, AdminUser from ..models import Asset, Node, SystemUser
from .. import serializers 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__ = [ __all__ = [
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi', 'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
'AssetUserExportViewSet',
] ]
@ -34,10 +31,17 @@ class AssetUserFilterBackend(filters.BaseFilterBackend):
value = request.GET.get(field) value = request.GET.get(field)
if not value: if not value:
continue 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 continue
elif field == "asset_id":
field = "asset"
kwargs[field] = value kwargs[field] = value
return queryset.filter(**kwargs) if kwargs:
queryset = queryset.filter(**kwargs)
logger.debug("Filter {}".format(kwargs))
return queryset
class AssetUserSearchBackend(filters.BaseFilterBackend): class AssetUserSearchBackend(filters.BaseFilterBackend):
@ -45,72 +49,63 @@ class AssetUserSearchBackend(filters.BaseFilterBackend):
value = request.GET.get('search') value = request.GET.get('search')
if not value: if not value:
return queryset return queryset
_queryset = AssetUserManager.none() queryset = queryset.search(value)
for field in view.search_fields: return queryset
if field in ("node_id", "system_user_id", "admin_user_id"):
continue
_queryset |= queryset.filter(**{field: value}) class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
return _queryset.distinct() def filter_queryset(self, request, queryset, view):
latest = request.GET.get('latest') == '1'
if latest:
queryset = queryset.distinct()
return queryset
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
serializer_class = serializers.AssetUserSerializer serializer_classes = {
'default': serializers.AssetUserWriteSerializer,
'list': serializers.AssetUserReadSerializer,
'retrieve': serializers.AssetUserReadSerializer,
}
permission_classes = [IsOrgAdminOrAppUser] permission_classes = [IsOrgAdminOrAppUser]
http_method_names = ['get', 'post']
filter_fields = [ filter_fields = [
"id", "ip", "hostname", "username", "asset_id", "node_id", "id", "ip", "hostname", "username",
"system_user_id", "admin_user_id" "asset_id", "node_id",
"prefer", "prefer_id",
] ]
search_fields = filter_fields search_fields = ["ip", "hostname", "username"]
filter_backends = ( filter_backends = [
filters.OrderingFilter,
AssetUserFilterBackend, AssetUserSearchBackend, AssetUserFilterBackend, AssetUserSearchBackend,
) AssetUserLatestFilterBackend,
]
def allow_bulk_destroy(self, qs, filtered): def allow_bulk_destroy(self, qs, filtered):
return False return False
def get_queryset(self): def get_object(self):
# 尽可能先返回更少的数据 pk = self.kwargs.get("pk")
username = self.request.GET.get('username') queryset = self.get_queryset()
asset_id = self.request.GET.get('asset_id') obj = queryset.get(id=pk)
node_id = self.request.GET.get('node_id') return obj
admin_user_id = self.request.GET.get("admin_user_id")
system_user_id = self.request.GET.get("system_user_id")
kwargs = {} def get_exception_handler(self):
assets = None def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler
def perform_destroy(self, instance):
manager = AssetUserManager() manager = AssetUserManager()
if system_user_id: manager.delete(instance)
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')
if asset_id: def get_queryset(self):
asset = get_object_or_404(Asset, id=asset_id) manager = AssetUserManager()
assets = [asset] queryset = manager.all()
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)
return queryset return queryset
class AssetUserExportViewSet(AssetUserViewSet): class AssetUserAuthInfoViewSet(AssetUserViewSet):
serializer_class = serializers.AssetUserExportSerializer serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
http_method_names = ['get'] http_method_names = ['get', 'post']
permission_classes = [IsOrgAdminOrAppUser] permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self): def get_permissions(self):
@ -119,66 +114,31 @@ class AssetUserExportViewSet(AssetUserViewSet):
return super().get_permissions() return super().get_permissions()
class AssetUserAuthInfoApi(generics.RetrieveAPIView): class AssetUserTaskCreateAPI(generics.CreateAPIView):
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
"""
permission_classes = (IsOrgAdminOrAppUser,) 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): 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() manager = AssetUserManager()
asset_users = manager.filter(username=username, assets=[asset], prefer=prefer) queryset = manager.all()
return asset_users 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() asset_users = self.get_asset_users()
prefer = self.request.GET.get("prefer") # action = serializer.validated_data["action"]
kwargs = {} # only this
if prefer == "admin_user": # if action == "test":
kwargs["run_as_admin"] = True task = test_asset_users_connectivity_manual.delay(asset_users)
task = test_asset_users_connectivity_manual.delay(asset_users, **kwargs) data = getattr(serializer, '_data', {})
return Response({"task": task.id}) data["task"] = task.id
setattr(serializer, '_data', data)
return task
def get_exception_handler(self):
class AssetUserPushApi(generics.CreateAPIView): def handler(e, context):
""" return Response({"error": str(e)}, status=400)
Test asset users connective return handler
"""
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

View File

@ -1,24 +1,11 @@
# ~*~ coding: utf-8 ~*~ # ~*~ 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 import status
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django.utils.translation import ugettext_lazy as _ 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.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer from common.tree import TreeNodeSerializer
@ -27,7 +14,8 @@ from orgs.mixins import generics
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import Node from ..models import Node
from ..tasks import ( 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 from .. import serializers
@ -36,9 +24,9 @@ logger = get_logger(__file__)
__all__ = [ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', 'NodeAddChildrenApi', 'NodeListAsTreeApi',
'TestNodeConnectiveApi', 'NodeListAsTreeApi', 'NodeChildrenAsTreeApi',
'NodeChildrenAsTreeApi', 'RefreshNodesCacheApi', 'NodeTaskCreateApi',
] ]
@ -64,9 +52,9 @@ class NodeViewSet(OrgModelViewSet):
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
node = self.get_object() node = self.get_object()
if node.has_children_or_contains_assets(): if node.has_children_or_has_assets():
msg = _("Deletion failed and the node contains children or assets") error = _("Deletion failed and the node contains children or assets")
return Response(data={'msg': msg}, status=status.HTTP_403_FORBIDDEN) return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs) return super().destroy(request, *args, **kwargs)
@ -261,41 +249,41 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
asset.nodes.set([instance]) asset.nodes.set([instance])
class RefreshNodeHardwareInfoApi(APIView): class NodeTaskCreateApi(generics.CreateAPIView):
model = Node model = Node
serializer_class = serializers.NodeTaskSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def get(self, request, *args, **kwargs): def get_object(self):
node_id = kwargs.get('pk') node_id = self.kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id) node = get_object_or_none(self.model, id=node_id)
assets = node.get_all_assets() return node
# 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})
@staticmethod
def set_serializer_data(s, task):
data = getattr(s, '_data', {})
data["task"] = task.id
setattr(s, '_data', data)
class TestNodeConnectiveApi(APIView): @staticmethod
permission_classes = (IsOrgAdmin,) def refresh_nodes_cache():
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):
Node.refresh_nodes() 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)

View File

@ -1,42 +1,25 @@
# ~*~ coding: utf-8 ~*~ # ~*~ 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 django.shortcuts import get_object_or_404
from rest_framework.response import Response 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.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from orgs.utils import tmp_to_org
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer
from ..tasks import ( from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual, 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__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
] ]
@ -48,13 +31,12 @@ class SystemUserViewSet(OrgBulkModelViewSet):
filter_fields = ("name", "username") filter_fields = ("name", "username")
search_fields = filter_fields search_fields = filter_fields
serializer_class = serializers.SystemUserSerializer serializer_class = serializers.SystemUserSerializer
serializer_classes = {
'default': serializers.SystemUserSerializer,
'list': serializers.SystemUserListSerializer,
}
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets'))
return queryset
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
""" """
@ -62,7 +44,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
""" """
model = SystemUser model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer serializer_class = SystemUserWithAuthInfoSerializer
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@ -75,88 +57,61 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
Get system user with asset auth info Get system user with asset auth info
""" """
model = SystemUser model = SystemUser
permission_classes = (IsAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer 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): def get_object(self):
instance = super().get_object() instance = super().get_object()
aid = self.kwargs.get('aid') username = instance.username
asset = get_object_or_404(Asset, pk=aid) if instance.username_same_with_user:
instance.load_specific_asset_auth(asset) 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 return instance
class SystemUserPushApi(generics.RetrieveAPIView): class SystemUserTaskApi(generics.CreateAPIView):
"""
Push system user to cluster assets api
"""
model = SystemUser
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = CeleryTaskSerializer serializer_class = serializers.SystemUserTaskSerializer
def retrieve(self, request, *args, **kwargs): def do_push(self, system_user, asset=None):
system_user = self.get_object() if asset is None:
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) task = push_system_user_to_assets_manual.delay(system_user)
return Response({"task": task.id}) else:
username = self.request.query_params.get('username')
task = push_system_user_a_asset_manual.delay(
system_user, asset, username=username
)
return task
@staticmethod
class SystemUserTestConnectiveApi(generics.RetrieveAPIView): def do_test(system_user, asset=None):
"""
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()
task = test_system_user_connectivity_manual.delay(system_user) task = test_system_user_connectivity_manual.delay(system_user)
return Response({"task": task.id}) return task
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self): def get_object(self):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=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() system_user = self.get_object()
return system_user.assets.all() if action == 'push':
task = self.do_push(system_user, asset)
else:
class SystemUserPushToAssetApi(generics.RetrieveAPIView): task = self.do_test(system_user, asset)
model = SystemUser data = getattr(serializer, '_data', {})
permission_classes = (IsOrgAdmin,) data["task"] = task.id
serializer_class = serializers.TaskIDSerializer setattr(serializer, '_data', data)
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})
class SystemUserCommandFilterRuleListApi(generics.ListAPIView): class SystemUserCommandFilterRuleListApi(generics.ListAPIView):

View File

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import defaultdict
from django.db.models import F, Value from django.db.models import F, Value
from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
@ -8,10 +10,13 @@ from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org from orgs.utils import current_org
from .. import models, serializers from .. import models, serializers
__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet'] __all__ = [
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
'SystemUserUserRelationViewSet',
]
class RelationMixin(OrgBulkModelViewSet): class RelationMixin:
def get_queryset(self): def get_queryset(self):
queryset = self.model.objects.all() queryset = self.model.objects.all()
org_id = current_org.org_id() org_id = current_org.org_id()
@ -23,8 +28,40 @@ class RelationMixin(OrgBulkModelViewSet):
)) ))
return queryset 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 serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -36,6 +73,9 @@ class SystemUserAssetRelationViewSet(RelationMixin):
"systemuser__name", "systemuser__username" "systemuser__name", "systemuser__username"
] ]
def get_objects_attr(self):
return models.Asset, 'asset'
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.annotate( queryset = queryset.annotate(
@ -47,7 +87,7 @@ class SystemUserAssetRelationViewSet(RelationMixin):
return queryset return queryset
class SystemUserNodeRelationViewSet(RelationMixin): class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -58,8 +98,39 @@ class SystemUserNodeRelationViewSet(RelationMixin):
"node__value", "systemuser__name", "systemuser_username" "node__value", "systemuser__name", "systemuser_username"
] ]
def get_objects_attr(self):
return models.Node, 'node'
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset \ queryset = queryset \
.annotate(node_key=F('node__key')) .annotate(node_key=F('node__key'))
return queryset 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

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
#
from ..models import AdminUser
from .asset_user import AssetUserBackend
class AdminUserBackend(AssetUserBackend):
model = AdminUser
backend = 'AdminUser'

View File

@ -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

View File

@ -1,94 +1,48 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid
from abc import abstractmethod from abc import abstractmethod
from ..models import Asset
class BaseBackend: class BaseBackend:
@classmethod
@abstractmethod @abstractmethod
def filter(cls, username=None, assets=None, latest=True, prefer=None, prefer_id=None): def all(self):
"""
:param username: 用户名
:param assets: <Asset>对象
:param latest: 是否是最新记录
:param prefer: 优先使用
:param prefer_id: 使用id
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
"""
pass pass
@abstractmethod
def filter(self, username=None, hostname=None, ip=None, assets=None,
node=None, prefer_id=None, **kwargs):
pass
class AssetUserQuerySet(list): @abstractmethod
def order_by(self, *ordering): def search(self, item):
_ordering = [] pass
reverse = False
for i in ordering:
if i[0] == '-':
reverse = True
i = i[1:]
_ordering.append(i)
self.sort(key=lambda obj: [getattr(obj, j) for j in _ordering], reverse=reverse)
return self
def filter_in(self, kwargs): @abstractmethod
in_kwargs = {} def get_queryset(self):
queryset = [] pass
for k, v in kwargs.items():
if len(v) == 0:
return self
if k.find("__in") >= 0:
_k = k.split('__')[0]
in_kwargs[_k] = v
else:
in_kwargs[k] = v
for k in in_kwargs:
kwargs.pop(k, None)
if len(in_kwargs) == 0: @abstractmethod
return self def delete(self, union_id):
for i in self: pass
matched = False
for k, v in in_kwargs.items():
attr = getattr(i, k, None)
# 如果属性或者value中是uuid,则转换成string
if isinstance(v[0], uuid.UUID):
v = [str(i) for i in v]
if isinstance(attr, uuid.UUID):
attr = str(attr)
if attr in v:
matched = True
if matched:
queryset.append(i)
return AssetUserQuerySet(queryset)
def filter_equal(self, kwargs): @staticmethod
def filter_it(obj): def qs_to_values(qs):
wanted = [] values = qs.values(
real = [] 'hostname', 'ip', "asset_id",
for k, v in kwargs.items(): 'username', 'password', 'private_key', 'public_key',
wanted.append(v) 'score', 'version',
value = getattr(obj, k, None) "asset_username", "union_id",
if isinstance(value, uuid.UUID): 'date_created', 'date_updated',
value = str(value) 'org_id', 'backend',
real.append(value) )
return wanted == real return values
kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1}
if len(kwargs) > 0:
queryset = AssetUserQuerySet([i for i in self if filter_it(i)])
else:
queryset = self
return queryset
def filter(self, **kwargs): @staticmethod
queryset = self.filter_in(kwargs).filter_equal(kwargs) def make_assets_as_id(assets):
return queryset if not assets:
return []
def distinct(self): if isinstance(assets[0], Asset):
items = list(set(self)) assets = [a.id for a in assets]
self[:] = items return assets
return self
def __or__(self, other):
self.extend(other)
return self

View File

@ -1,29 +1,318 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext as _
from functools import reduce
from django.db.models import F, CharField, Value, IntegerField, Q, Count
from django.db.models.functions import Concat
from ..models import AuthBook from common.utils import get_object_or_none
from orgs.utils import current_org
from ..models import AuthBook, SystemUser, Asset, AdminUser
from .base import BaseBackend from .base import BaseBackend
class AuthBookBackend(BaseBackend): class DBBackend(BaseBackend):
@classmethod union_id_length = 2
def filter(cls, username=None, assets=None, latest=True, **kwargs):
queryset = AuthBook.objects.all()
if username is not None:
queryset = queryset.filter(username=username)
if assets:
queryset = queryset.filter(asset__in=assets)
if latest:
queryset = queryset.latest_version()
return queryset
@classmethod def __init__(self, queryset=None):
def create(cls, **kwargs): if queryset is None:
auth_info = { queryset = self.all()
'password': kwargs.pop('password', ''), self.queryset = queryset
'public_key': kwargs.pop('public_key', ''),
'private_key': kwargs.pop('private_key', '') def _clone(self):
} return self.__class__(self.queryset)
obj = AuthBook.objects.create(**kwargs)
obj.set_auth(**auth_info) def all(self):
return obj return AuthBook.objects.none()
def count(self):
return self.queryset.count()
def get_queryset(self):
return self.queryset
def delete(self, union_id):
cleaned_union_id = union_id.split('_')
# 如果union_id通不过本检查代表可能不是本backend, 应该返回空
if not self._check_union_id(union_id, cleaned_union_id):
return
return self._perform_delete_by_union_id(cleaned_union_id)
def _perform_delete_by_union_id(self, union_id_cleaned):
pass
def filter(self, assets=None, node=None, prefer=None, prefer_id=None,
union_id=None, id__in=None, **kwargs):
clone = self._clone()
clone._filter_union_id(union_id)
clone._filter_prefer(prefer, prefer_id)
clone._filter_node(node)
clone._filter_assets(assets)
clone._filter_other(kwargs)
clone._filter_id_in(id__in)
return clone
def _filter_union_id(self, union_id):
if not union_id:
return
cleaned_union_id = union_id.split('_')
# 如果union_id通不过本检查代表可能不是本backend, 应该返回空
if not self._check_union_id(union_id, cleaned_union_id):
self.queryset = self.queryset.none()
return
return self._perform_filter_union_id(union_id, cleaned_union_id)
def _check_union_id(self, union_id, cleaned_union_id):
return union_id and len(cleaned_union_id) == self.union_id_length
def _perform_filter_union_id(self, union_id, union_id_cleaned):
self.queryset = self.queryset.filter(union_id=union_id)
def _filter_assets(self, assets):
assets_id = self.make_assets_as_id(assets)
if assets_id:
self.queryset = self.queryset.filter(asset_id__in=assets_id)
def _filter_node(self, node):
pass
def _filter_id_in(self, ids):
if ids and isinstance(ids, list):
self.queryset = self.queryset.filter(union_id__in=ids)
@staticmethod
def clean_kwargs(kwargs):
return {k: v for k, v in kwargs.items() if v}
def _filter_other(self, kwargs):
kwargs = self.clean_kwargs(kwargs)
if kwargs:
self.queryset = self.queryset.filter(**kwargs)
def _filter_prefer(self, prefer, prefer_id):
pass
def search(self, item):
qs = []
for i in ['hostname', 'ip', 'username']:
kwargs = {i + '__startswith': item}
qs.append(Q(**kwargs))
q = reduce(lambda x, y: x | y, qs)
clone = self._clone()
clone.queryset = clone.queryset.filter(q).distinct()
return clone
class SystemUserBackend(DBBackend):
model = SystemUser.assets.through
backend = 'system_user'
prefer = backend
base_score = 0
union_id_length = 2
def _filter_prefer(self, prefer, prefer_id):
if prefer and prefer != self.prefer:
self.queryset = self.queryset.none()
if prefer_id:
self.queryset = self.queryset.filter(systemuser__id=prefer_id)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
system_user_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
asset_id=asset_id, systemuser__id=system_user_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
system_user_id, asset_id = union_id_cleaned
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset = get_object_or_none(Asset, pk=asset_id)
if all((system_user, asset)):
system_user.assets.remove(asset)
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
def get_annotate(self):
kwargs = dict(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
username=F("systemuser__username"),
password=F("systemuser__password"),
private_key=F("systemuser__private_key"),
public_key=F("systemuser__public_key"),
score=F("systemuser__priority") + self.base_score,
version=Value(0, IntegerField()),
date_created=F("systemuser__date_created"),
date_updated=F("systemuser__date_updated"),
asset_username=Concat(F("asset__id"), Value("_"),
F("systemuser__username"),
output_field=CharField()),
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
output_field=CharField()),
org_id=F("asset__org_id"),
backend=Value(self.backend, CharField())
)
return kwargs
def get_filter(self):
return dict(
systemuser__username_same_with_user=False,
)
def all(self):
kwargs = self.get_annotate()
filters = self.get_filter()
qs = self.model.objects.all().annotate(**kwargs)
if current_org.org_id() is not None:
filters['org_id'] = current_org.org_id()
qs = qs.filter(**filters)
qs = self.qs_to_values(qs)
return qs
class DynamicSystemUserBackend(SystemUserBackend):
backend = 'system_user_dynamic'
prefer = 'system_user'
union_id_length = 3
def get_annotate(self):
kwargs = super().get_annotate()
kwargs.update(dict(
username=F("systemuser__users__username"),
asset_username=Concat(
F("asset__id"), Value("_"),
F("systemuser__users__username"),
output_field=CharField()
),
union_id=Concat(
F("systemuser_id"), Value("_"), F("asset_id"),
Value("_"), F("systemuser__users__id"),
output_field=CharField()
),
users_count=Count('systemuser__users'),
))
return kwargs
def _perform_filter_union_id(self, union_id, union_id_cleaned):
system_user_id, asset_id, user_id = union_id_cleaned
self.queryset = self.queryset.filter(
asset_id=asset_id, systemuser_id=system_user_id,
union_id=union_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
system_user_id, asset_id, user_id = union_id_cleaned
system_user = get_object_or_none(SystemUser, pk=system_user_id)
if not system_user:
return
system_user.users.remove(user_id)
if system_user.users.count() == 0:
system_user.assets.remove(asset_id)
def get_filter(self):
return dict(
users_count__gt=0,
systemuser__username_same_with_user=True
)
class AdminUserBackend(DBBackend):
model = Asset
backend = 'admin_user'
prefer = backend
base_score = 200
def _filter_prefer(self, prefer, prefer_id):
if prefer and prefer != self.backend:
self.queryset = self.queryset.none()
if prefer_id:
self.queryset = self.queryset.filter(admin_user__id=prefer_id)
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(nodes__id=node.id)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
admin_user_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
id=asset_id, admin_user_id=admin_user_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
raise PermissionError(_("Could not remove asset admin user"))
def all(self):
qs = self.model.objects.all().annotate(
asset_id=F("id"),
username=F("admin_user__username"),
password=F("admin_user__password"),
private_key=F("admin_user__private_key"),
public_key=F("admin_user__public_key"),
score=Value(self.base_score, IntegerField()),
version=Value(0, IntegerField()),
date_updated=F("admin_user__date_updated"),
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
)
qs = self.qs_to_values(qs)
return qs
class AuthbookBackend(DBBackend):
model = AuthBook
backend = 'db'
prefer = backend
base_score = 400
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
def _filter_prefer(self, prefer, prefer_id):
if not prefer or not prefer_id:
return
if prefer.lower() == "admin_user":
model = AdminUser
elif prefer.lower() == "system_user":
model = SystemUser
else:
self.queryset = self.queryset.none()
return
obj = get_object_or_none(model, pk=prefer_id)
if obj is None:
self.queryset = self.queryset.none()
return
username = obj.get_username()
if isinstance(username, str):
self.queryset = self.queryset.filter(username=username)
# dynamic system user return more username
else:
self.queryset = self.queryset.filter(username__in=username)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
authbook_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
id=authbook_id, asset_id=asset_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
authbook_id, asset_id = union_id_cleaned
authbook = get_object_or_none(AuthBook, pk=authbook_id)
if authbook.is_latest:
raise PermissionError(_("Latest version could not be delete"))
AuthBook.objects.filter(id=authbook_id).delete()
def all(self):
qs = self.model.objects.all().annotate(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
score=F('version') + self.base_score,
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
)
qs = self.qs_to_values(qs)
return qs

View File

@ -1,110 +1,162 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from itertools import chain, groupby
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from .base import AssetUserQuerySet from orgs.utils import current_org
from .db import AuthBookBackend from common.utils import get_logger, lazyproperty
from .system_user import SystemUserBackend from common.struct import QuerySetChain
from .admin_user import AdminUserBackend
from ..models import AssetUser, AuthBook
from .db import (
AuthbookBackend, SystemUserBackend, AdminUserBackend,
DynamicSystemUserBackend
)
logger = get_logger(__name__)
class NotSupportError(Exception): class NotSupportError(Exception):
pass pass
class AssetUserManager: class AssetUserQueryset:
"""
资产用户管理器
"""
ObjectDoesNotExist = ObjectDoesNotExist ObjectDoesNotExist = ObjectDoesNotExist
MultipleObjectsReturned = MultipleObjectsReturned MultipleObjectsReturned = MultipleObjectsReturned
NotSupportError = NotSupportError
MSG_NOT_EXIST = '{} Object matching query does not exist'
MSG_MULTIPLE = '{} get() returned more than one object ' \
'-- it returned {}!'
backends = ( def __init__(self, backends=()):
('db', AuthBookBackend), self.backends = backends
self._distinct_queryset = None
def backends_queryset(self):
return [b.get_queryset() for b in self.backends]
@lazyproperty
def backends_counts(self):
return [b.count() for b in self.backends]
def filter(self, hostname=None, ip=None, username=None,
assets=None, asset=None, node=None,
id=None, prefer_id=None, prefer=None, id__in=None):
if not assets and asset:
assets = [asset]
kwargs = dict(
hostname=hostname, ip=ip, username=username,
assets=assets, node=node, prefer=prefer, prefer_id=prefer_id,
id__in=id__in, union_id=id,
)
logger.debug("Filter: {}".format(kwargs))
backends = []
for backend in self.backends:
clone = backend.filter(**kwargs)
backends.append(clone)
return self._clone(backends)
def _clone(self, backends=None):
if backends is None:
backends = self.backends
return self.__class__(backends)
def search(self, item):
backends = []
for backend in self.backends:
new = backend.search(item)
backends.append(new)
return self._clone(backends)
def distinct(self):
logger.debug("Distinct asset user queryset")
queryset_chain = chain(*(backend.get_queryset() for backend in self.backends))
queryset_sorted = sorted(
queryset_chain,
key=lambda item: (item["asset_username"], item["score"]),
reverse=True,
)
results = groupby(queryset_sorted, key=lambda item: item["asset_username"])
final = [next(result[1]) for result in results]
self._distinct_queryset = final
return self
def get(self, latest=False, **kwargs):
queryset = self.filter(**kwargs)
if latest:
queryset = queryset.distinct()
queryset = list(queryset)
count = len(queryset)
if count == 1:
data = queryset[0]
return data
elif count > 1:
msg = 'Should return 1 record, but get {}'.format(count)
raise MultipleObjectsReturned(msg)
else:
msg = 'No record found(org is {})'.format(current_org.name)
raise ObjectDoesNotExist(msg)
def get_latest(self, **kwargs):
return self.get(latest=True, **kwargs)
@staticmethod
def to_asset_user(data):
obj = AssetUser()
for k, v in data.items():
setattr(obj, k, v)
return obj
@property
def queryset(self):
if self._distinct_queryset is not None:
return self._distinct_queryset
return QuerySetChain(self.backends_queryset())
def count(self):
if self._distinct_queryset is not None:
return len(self._distinct_queryset)
else:
return sum(self.backends_counts)
def __getitem__(self, ndx):
return self.queryset.__getitem__(ndx)
def __iter__(self):
self._data = iter(self.queryset)
return self
def __next__(self):
return self.to_asset_user(next(self._data))
class AssetUserManager:
support_backends = (
('db', AuthbookBackend),
('system_user', SystemUserBackend), ('system_user', SystemUserBackend),
('admin_user', AdminUserBackend), ('admin_user', AdminUserBackend),
('system_user_dynamic', DynamicSystemUserBackend),
) )
_prefer = "system_user" def __init__(self):
self.backends = [backend() for name, backend in self.support_backends]
self._queryset = AssetUserQueryset(self.backends)
def filter(self, username=None, assets=None, latest=True, prefer=None, prefer_id=None): def all(self):
if assets is not None and not assets: return self._queryset
return AssetUserQuerySet([])
if prefer: def delete(self, obj):
self._prefer = prefer name_backends_map = dict(self.support_backends)
backend_name = obj.backend
instances_map = {} backend_cls = name_backends_map.get(backend_name)
instances = [] union_id = obj.union_id
for name, backend in self.backends: if backend_cls:
# if name != "db": backend_cls().delete(union_id)
# continue
_instances = backend.filter(
username=username, assets=assets, latest=latest,
prefer=self._prefer, prefer_id=prefer_id,
)
instances_map[name] = _instances
# 如果不是获取最新版本就不再merge
if not latest:
for _instances in instances_map.values():
instances.extend(_instances)
return AssetUserQuerySet(instances)
# merge的顺序
ordering = ["db"]
if self._prefer == "system_user":
ordering.extend(["system_user", "admin_user"])
else: else:
ordering.extend(["admin_user", "system_user"]) raise ObjectDoesNotExist("Not backend found")
# 根据prefer决定优先使用系统用户或管理用户谁的
ordering_instances = [instances_map.get(i, []) for i in ordering]
instances = self._merge_instances(*ordering_instances)
return AssetUserQuerySet(instances)
def get(self, username, asset, **kwargs):
instances = self.filter(username, assets=[asset], **kwargs)
if len(instances) == 1:
return instances[0]
elif len(instances) == 0:
self.raise_does_not_exist(self.__class__.__name__)
else:
self.raise_multiple_return(self.__class__.__name__, len(instances))
def raise_does_not_exist(self, name):
raise self.ObjectDoesNotExist(self.MSG_NOT_EXIST.format(name))
def raise_multiple_return(self, name, length):
raise self.MultipleObjectsReturned(self.MSG_MULTIPLE.format(name, length))
@staticmethod @staticmethod
def create(**kwargs): def create(**kwargs):
instance = AuthBookBackend.create(**kwargs) authbook = AuthBook(**kwargs)
return instance authbook.save()
return authbook
def all(self): def __getattr__(self, item):
return self.filter() return getattr(self._queryset, item)
def prefer(self, s):
self._prefer = s
return self
@staticmethod
def none():
return AssetUserQuerySet()
@staticmethod
def _merge_instances(*args):
instances = list(args[0])
keywords = [obj.keyword for obj in instances]
for _instances in args[1:]:
need_merge_instances = [obj for obj in _instances if obj.keyword not in keywords]
need_merge_keywords = [obj.keyword for obj in need_merge_instances]
instances.extend(need_merge_instances)
keywords.extend(need_merge_keywords)
return instances

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
#
import itertools
from assets.models import SystemUser
from .asset_user import AssetUserBackend
class SystemUserBackend(AssetUserBackend):
model = SystemUser
backend = 'SystemUser'
@classmethod
def filter_queryset_more(cls, queryset):
queryset = cls._distinct_system_users_by_username(queryset)
return queryset
@classmethod
def _distinct_system_users_by_username(cls, system_users):
system_users = sorted(
system_users,
key=lambda su: (su.username, su.priority, su.date_updated),
reverse=True,
)
results = itertools.groupby(system_users, key=lambda su: su.username)
system_users = [next(result[1]) for result in results]
return system_users

View File

@ -3,14 +3,5 @@
# from django.conf import settings # from django.conf import settings
from .db import AuthBookBackend
# from .vault import VaultBackend # from .vault import VaultBackend
def get_backend():
default_backend = AuthBookBackend
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
# return VaultBackend
return default_backend

View File

@ -1,11 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .base import BaseBackend
class VaultBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, latest=True):
pass

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from itertools import groupby
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.utils import get_logger from common.utils import get_logger
from orgs.mixins.forms import OrgModelForm from orgs.mixins.forms import OrgModelForm
from ..models import Asset from ..models import Asset, Platform
logger = get_logger(__file__) logger = get_logger(__file__)
@ -42,9 +43,26 @@ class AssetCreateUpdateForm(OrgModelForm):
] ]
nodes_field.choices = nodes_choices nodes_field.choices = nodes_choices
@staticmethod
def sorted_platform(platform):
if platform['base'] == 'Other':
return 'zz'
return platform['base']
def set_platform_to_name(self): def set_platform_to_name(self):
choices = []
platforms = Platform.objects.all().values('name', 'base')
platforms_sorted = sorted(platforms, key=self.sorted_platform)
platforms_grouped = groupby(platforms_sorted, key=lambda x: x['base'])
for i in platforms_grouped:
base = i[0]
grouped = sorted(i[1], key=lambda x: x['name'])
grouped = [(j['name'], j['name']) for j in grouped]
choices.append(
(base, grouped)
)
platform_field = self.fields['platform'] platform_field = self.fields['platform']
platform_field.to_field_name = 'name' platform_field.choices = choices
if self.instance: if self.instance:
self.initial['platform'] = self.instance.platform.name self.initial['platform'] = self.instance.platform.name

View File

@ -88,7 +88,9 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
fields = [ fields = [
'name', 'username', 'protocol', 'auto_generate_key', 'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key', 'auto_push', 'sudo', 'password', 'private_key', 'auto_push', 'sudo',
'username_same_with_user',
'comment', 'shell', 'priority', 'login_mode', 'cmd_filters', 'comment', 'shell', 'priority', 'login_mode', 'cmd_filters',
'sftp_root',
] ]
widgets = { widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'name': forms.TextInput(attrs={'placeholder': _('Name')}),
@ -97,11 +99,17 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
'class': 'select2', 'data-placeholder': _('Command filter') 'class': 'select2', 'data-placeholder': _('Command filter')
}), }),
} }
labels = {
'username_same_with_user': _("Username same with user"),
}
help_texts = { help_texts = {
'auto_push': _('Auto push system user to asset'), 'auto_push': _('Auto push system user to asset'),
'priority': _('1-100, High level will be using login asset as default, ' 'priority': _('1-100, High level will be using login asset as default, '
'if user was granted more than 2 system user'), 'if user was granted more than 2 system user'),
'login_mode': _('If you choose manual login mode, you do not ' 'login_mode': _('If you choose manual login mode, you do not '
'need to fill in the username and password.'), 'need to fill in the username and password.'),
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig") 'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig"),
'sftp_root': _("SFTP root dir, tmp, home or custom"),
'username_same_with_user': _("Username is dynamic, When connect asset, using current user's username"),
# 'username_same_with_user': _("用户名是动态的,登录资产时使用当前用户的用户名登录"),
} }

View File

@ -6,7 +6,7 @@
Other module of this app shouldn't connect with other app. 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. :license: GPL v2, see LICENSE for more details.
""" """

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.7 on 2020-01-06 07:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0046_auto_20191218_1705'),
]
operations = [
migrations.CreateModel(
name='AssetUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.authbook',),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.7 on 2019-12-30 07:12
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0047_assetuser'),
]
operations = [
migrations.RemoveField(
model_name='authbook',
name='is_active',
),
migrations.AddField(
model_name='systemuser',
name='username_same_with_user',
field=models.BooleanField(default=False, verbose_name='Username same with user'),
),
migrations.AddField(
model_name='systemuser',
name='users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'),
),
migrations.AddField(
model_name='systemuser',
name='groups',
field=models.ManyToManyField(blank=True, to='users.UserGroup',
verbose_name='User groups'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-19 07:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0048_auto_20191230_1512'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='sftp_root',
field=models.CharField(default='tmp', max_length=128, verbose_name='SFTP Root'),
),
]

View File

@ -1,6 +1,8 @@
from .base import *
from .asset import * from .asset import *
from .label import Label from .label import Label
from .user import * from .user import *
from .asset_user import *
from .cluster import * from .cluster import *
from .group import * from .group import *
from .domain import * from .domain import *

View File

@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
from common.fields.model import JsonDictTextField from common.fields.model import JsonDictTextField
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import ConnectivityMixin
from .utils import Connectivity from .utils import Connectivity
__all__ = ['Asset', 'ProtocolsMixin', 'Platform'] __all__ = ['Asset', 'ProtocolsMixin', 'Platform']
@ -40,10 +41,11 @@ def default_node():
class AssetManager(OrgManager): class AssetManager(OrgManager):
def get_queryset(self): # def get_queryset(self):
return super().get_queryset().annotate( # return super().get_queryset().annotate(
platform_base=models.F('platform__base') # platform_base=models.F('platform__base')
) # )
pass
class AssetQuerySet(models.QuerySet): class AssetQuerySet(models.QuerySet):
@ -243,6 +245,13 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def platform_base(self): def platform_base(self):
return self.platform.base return self.platform.base
@lazyproperty
def admin_user_username(self):
"""求可连接性时直接用用户名去取避免再查一次admin user
serializer 中直接通过annotate方式返回了这个
"""
return self.admin_user.username
def is_windows(self): def is_windows(self):
return self.platform.is_windows() return self.platform.is_windows()
@ -275,9 +284,11 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def connectivity(self): def connectivity(self):
if self._connectivity: if self._connectivity:
return self._connectivity return self._connectivity
if not self.admin_user: if not self.admin_user_username:
return Connectivity.unknown() return Connectivity.unknown()
connectivity = self.admin_user.get_asset_connectivity(self) connectivity = ConnectivityMixin.get_asset_username_connectivity(
self, self.admin_user_username
)
return connectivity return connectivity
@connectivity.setter @connectivity.setter
@ -290,7 +301,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
if not self.admin_user: if not self.admin_user:
return {} return {}
self.admin_user.load_specific_asset_auth(self) self.admin_user.load_asset_special_auth(self)
info = { info = {
'username': self.admin_user.username, 'username': self.admin_user.username,
'password': self.admin_user.password, 'password': self.admin_user.password,

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
#
from .authbook import AuthBook
class AssetUser(AuthBook):
hostname = ""
ip = ""
backend = ""
union_id = ""
asset_username = ""
class Meta:
proxy = True

View File

@ -5,26 +5,24 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
from .base import AssetUser from .base import BaseUser
__all__ = ['AuthBook'] __all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet): class AuthBookQuerySet(models.QuerySet):
def latest_version(self): def latest_version(self):
return self.filter(is_latest=True).filter(is_active=True) return self.filter(is_latest=True)
class AuthBookManager(OrgManager): class AuthBookManager(OrgManager):
pass pass
class AuthBook(AssetUser): class AuthBook(BaseUser):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version')) is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
version = models.IntegerField(default=1, verbose_name=_('Version')) version = models.IntegerField(default=1, verbose_name=_('Version'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
objects = AuthBookManager.from_queryset(AuthBookQuerySet)() objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
backend = "db" backend = "db"

View File

@ -12,98 +12,29 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from common.utils import ( from common.utils import (
signer, ssh_key_string_to_obj, ssh_key_gen, get_logger ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
) )
from common.validators import alphanumeric from common.validators import alphanumeric
from common import fields from common import fields
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .utils import private_key_validator, Connectivity from .utils import Connectivity
logger = get_logger(__file__) logger = get_logger(__file__)
class AssetUser(OrgModelMixin): class ConnectivityMixin:
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY" CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY"
CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT" CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT"
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
ASSET_USER_CACHE_TIME = 3600 * 24 ASSET_USER_CACHE_TIME = 3600 * 24
id = ''
_prefer = "system_user" username = ''
_assets_amount = None
@property
def private_key_obj(self):
if self.private_key:
return ssh_key_string_to_obj(self.private_key, password=self.password)
else:
return None
@property
def private_key_file(self):
if not self.private_key_obj:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
@property
def public_key_obj(self):
if self.public_key:
try:
return sshpubkeys.SSHKey(self.public_key)
except TabError:
pass
return None
@property @property
def part_id(self): def part_id(self):
i = '-'.join(str(self.id).split('-')[:3]) i = '-'.join(str(self.id).split('-')[:3])
return i return i
def get_private_key(self):
if not self.private_key_obj:
return None
string_io = io.StringIO()
self.private_key_obj.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
def get_related_assets(self):
assets = self.assets.all()
return assets
def set_auth(self, password=None, private_key=None, public_key=None):
update_fields = []
if password:
self.password = password
update_fields.append('password')
if private_key:
self.private_key = private_key
update_fields.append('private_key')
if public_key:
self.public_key = public_key
update_fields.append('public_key')
if update_fields:
self.save(update_fields=update_fields)
def set_connectivity(self, summary): def set_connectivity(self, summary):
unreachable = summary.get('dark', {}).keys() unreachable = summary.get('dark', {}).keys()
reachable = summary.get('contacted', {}).keys() reachable = summary.get('contacted', {}).keys()
@ -150,20 +81,10 @@ class AssetUser(OrgModelMixin):
cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME) cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME)
return amount return amount
@property @classmethod
def assets_amount(self): def get_asset_username_connectivity(cls, asset, username):
if self._assets_amount is not None: key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id)
return self._assets_amount return Connectivity.get(key)
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cached = cache.get(cache_key)
if not cached:
cached = self.get_related_assets().count()
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
return cached
def expire_assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cache.delete(cache_key)
def get_asset_connectivity(self, asset): def get_asset_connectivity(self, asset):
key = self.get_asset_connectivity_key(asset) key = self.get_asset_connectivity_key(asset)
@ -176,28 +97,103 @@ class AssetUser(OrgModelMixin):
key = self.get_asset_connectivity_key(asset) key = self.get_asset_connectivity_key(asset)
Connectivity.set(key, c) Connectivity.set(key, c)
def get_asset_user(self, asset):
from ..backends import AssetUserManager class AuthMixin:
private_key = ''
password = ''
public_key = ''
username = ''
_prefer = 'system_user'
@property
def private_key_obj(self):
if self.private_key:
key_obj = ssh_key_string_to_obj(self.private_key, password=self.password)
return key_obj
else:
return None
@property
def private_key_file(self):
if not self.private_key_obj:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
def get_private_key(self):
if not self.private_key_obj:
return None
string_io = io.StringIO()
self.private_key_obj.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
@property
def public_key_obj(self):
if self.public_key:
try: try:
manager = AssetUserManager().prefer(self._prefer) return sshpubkeys.SSHKey(self.public_key)
other = manager.get(username=self.username, asset=asset, prefer_id=self.id) except TabError:
pass
return None
def set_auth(self, password=None, private_key=None, public_key=None):
update_fields = []
if password:
self.password = password
update_fields.append('password')
if private_key:
self.private_key = private_key
update_fields.append('private_key')
if public_key:
self.public_key = public_key
update_fields.append('public_key')
if update_fields:
self.save(update_fields=update_fields)
def has_special_auth(self, asset=None):
from .authbook import AuthBook
queryset = AuthBook.objects.filter(username=self.username)
if asset:
queryset = queryset.filter(asset=asset)
return queryset.exists()
def get_asset_user(self, asset, username=None):
from ..backends import AssetUserManager
if username is None:
username = self.username
try:
manager = AssetUserManager()
other = manager.get_latest(
username=username, asset=asset,
prefer_id=self.id, prefer=self._prefer,
)
return other return other
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return None return None
def load_specific_asset_auth(self, asset): def load_asset_special_auth(self, asset=None, username=None):
instance = self.get_asset_user(asset) if not asset:
return self
instance = self.get_asset_user(asset, username=username)
if instance: if instance:
self._merge_auth(instance) self._merge_auth(instance)
def _merge_auth(self, other): def _merge_auth(self, other):
if other.password: if other.password:
self.password = other.password self.password = other.password
if other.public_key: if other.public_key or other.private_key:
self.public_key = other.public_key
if other.private_key:
self.private_key = other.private_key self.private_key = other.private_key
self.public_key = other.public_key
def clear_auth(self): def clear_auth(self):
self.password = '' self.password = ''
@ -216,19 +212,57 @@ class AssetUser(OrgModelMixin):
) )
return private_key, public_key return private_key, public_key
def auto_gen_auth(self): def auto_gen_auth(self, password=True, key=True):
password = str(uuid.uuid4()) _password = None
private_key, public_key = ssh_key_gen( _private_key = None
username=self.username _public_key = None
)
if password:
_password = self.gen_password()
if key:
_private_key, _public_key = self.gen_key(self.username)
self.set_auth( self.set_auth(
password=password, private_key=private_key, password=_password, private_key=_private_key,
public_key=public_key public_key=_public_key
) )
def auto_gen_auth_password(self):
password = str(uuid.uuid4()) class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
self.set_auth(password=password) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
ASSET_USER_CACHE_TIME = 600
_prefer = "system_user"
def get_related_assets(self):
assets = self.assets.filter(org_id=self.org_id)
return assets
def get_username(self):
return self.username
@lazyproperty
def assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cached = cache.get(cache_key)
if not cached:
cached = self.get_related_assets().count()
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
return cached
def expire_assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cache.delete(cache_key)
def _to_secret_json(self): def _to_secret_json(self):
"""Push system user use it""" """Push system user use it"""
@ -240,26 +274,6 @@ class AssetUser(OrgModelMixin):
'private_key': self.private_key_file, 'private_key': self.private_key_file,
} }
def generate_id_with_asset(self, asset):
user_id = [self.part_id]
asset_id = str(asset.id).split('-')[3:]
ids = user_id + asset_id
return '-'.join(ids)
def construct_to_authbook(self, asset):
from . import AuthBook
fields = [
'name', 'username', 'comment', 'org_id',
'password', 'private_key', 'public_key',
'date_created', 'date_updated', 'created_by'
]
i = self.generate_id_with_asset(asset)
obj = AuthBook(id=i, asset=asset, version=0, is_latest=True)
for field in fields:
value = getattr(self, field)
setattr(obj, field, value)
return obj
class Meta: class Meta:
abstract = True abstract = True

View File

@ -10,7 +10,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .base import AssetUser from .base import BaseUser
__all__ = ['Domain', 'Gateway'] __all__ = ['Domain', 'Gateway']
@ -39,7 +39,7 @@ class Domain(OrgModelMixin):
return random.choice(self.gateways) return random.choice(self.gateways)
class Gateway(AssetUser): class Gateway(BaseUser):
PROTOCOL_SSH = 'ssh' PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp' PROTOCOL_RDP = 'rdp'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (

View File

@ -11,9 +11,9 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.core.cache import cache from django.core.cache import cache
from common.utils import get_logger, timeit, lazyproperty from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import set_current_org, get_current_org, tmp_to_org from orgs.utils import get_current_org, tmp_to_org, current_org
from orgs.models import Organization from orgs.models import Organization
@ -26,63 +26,108 @@ class NodeQuerySet(models.QuerySet):
raise PermissionError("Bulk delete node deny") raise PermissionError("Bulk delete node deny")
class TreeCache:
updated_time_cache_key = 'NODE_TREE_UPDATED_AT_{}'
cache_time = 3600
assets_updated_time_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT_{}'
def __init__(self, tree, org_id):
now = time.time()
self.created_time = now
self.assets_created_time = now
self.tree = tree
self.org_id = org_id
def _has_changed(self, tp="tree"):
if tp == "assets":
key = self.assets_updated_time_cache_key.format(self.org_id)
else:
key = self.updated_time_cache_key.format(self.org_id)
updated_time = cache.get(key, 0)
if updated_time > self.created_time:
return True
else:
return False
@classmethod
def set_changed(cls, tp="tree", t=None, org_id=None):
if org_id is None:
org_id = current_org.id
if tp == "assets":
key = cls.assets_updated_time_cache_key.format(org_id)
else:
key = cls.updated_time_cache_key.format(org_id)
ttl = cls.cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
def tree_has_changed(self):
return self._has_changed("tree")
def set_tree_changed(self, t=None):
logger.debug("Set tree tree changed")
self.__class__.set_changed(t=t, tp="tree")
def assets_has_changed(self):
return self._has_changed("assets")
def set_tree_assets_changed(self, t=None):
logger.debug("Set tree assets changed")
self.__class__.set_changed(t=t, tp="assets")
def get(self):
if self.tree_has_changed():
self.renew()
return self.tree
if self.assets_has_changed():
self.tree.init_assets()
return self.tree
def renew(self):
new_obj = self.__class__.new(self.org_id)
self.tree = new_obj.tree
self.created_time = new_obj.created_time
self.assets_created_time = new_obj.assets_created_time
@classmethod
def new(cls, org_id=None):
from ..utils import TreeService
logger.debug("Create node tree")
if not org_id:
org_id = current_org.id
with tmp_to_org(org_id):
tree = TreeService.new()
obj = cls(tree, org_id)
obj.tree = tree
return obj
class TreeMixin: class TreeMixin:
tree_created_time = None _org_tree_map = {}
tree_updated_time_cache_key = 'NODE_TREE_UPDATED_AT'
tree_cache_time = 3600
tree_assets_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT'
tree_assets_created_time = None
_tree_service = None
@classmethod @classmethod
def tree(cls): def tree(cls):
from ..utils import TreeService org_id = current_org.org_id()
tree_updated_time = cache.get(cls.tree_updated_time_cache_key, 0) t = cls.get_local_tree_cache(org_id)
now = time.time()
# 什么时候重新初始化 _tree_service if t is None:
if not cls.tree_created_time or \ t = TreeCache.new()
tree_updated_time > cls.tree_created_time: cls._org_tree_map[org_id] = t
logger.debug("Create node tree") return t.get()
tree = TreeService.new()
cls.tree_created_time = now @classmethod
cls.tree_assets_created_time = now def get_local_tree_cache(cls, org_id=None):
cls._tree_service = tree t = cls._org_tree_map.get(org_id)
return tree return t
# 是否要重新初始化节点资产
node_assets_updated_time = cache.get(cls.tree_assets_cache_key, 0)
if not cls.tree_assets_created_time or \
node_assets_updated_time > cls.tree_assets_created_time:
cls._tree_service.init_assets()
cls.tree_assets_created_time = now
logger.debug("Refresh node tree assets")
return cls._tree_service
@classmethod @classmethod
def refresh_tree(cls, t=None): def refresh_tree(cls, t=None):
logger.debug("Refresh node tree") TreeCache.set_changed(tp="tree", t=t, org_id=current_org.id)
key = cls.tree_updated_time_cache_key
ttl = cls.tree_cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
@classmethod @classmethod
def refresh_node_assets(cls, t=None): def refresh_node_assets(cls, t=None):
logger.debug("Refresh node assets") TreeCache.set_changed(tp="assets", t=t, org_id=current_org.id)
key = cls.tree_assets_cache_key
ttl = cls.tree_cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
@staticmethod
def refresh_user_tree_cache():
"""
当节点-节点关系节点-资产关系发生变化时应该刷新用户授权树缓存
:return:
"""
from perms.utils.asset_permission import AssetPermissionUtilV2
AssetPermissionUtilV2.expire_all_user_tree_cache()
class FamilyMixin: class FamilyMixin:
@ -376,15 +421,6 @@ class SomeNodesMixin:
) )
return obj return obj
@classmethod
def empty_node(cls):
with tmp_to_org(Organization.system()):
defaults = {'value': cls.empty_value}
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.empty_key
)
return obj
@classmethod @classmethod
def default_node(cls): def default_node(cls):
with tmp_to_org(Organization.default()): with tmp_to_org(Organization.default()):
@ -413,7 +449,6 @@ class SomeNodesMixin:
@classmethod @classmethod
def initial_some_nodes(cls): def initial_some_nodes(cls):
cls.default_node() cls.default_node()
cls.empty_node()
cls.ungrouped_node() cls.ungrouped_node()
cls.favorite_node() cls.favorite_node()
@ -523,13 +558,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
tree_node = TreeNode(**data) tree_node = TreeNode(**data)
return tree_node return tree_node
def has_children_or_contains_assets(self): def has_children_or_has_assets(self):
if self.children or self.get_assets(): if self.children or self.get_assets().exists():
return True return True
return False return False
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
if self.has_children_or_contains_assets(): if self.has_children_or_has_assets():
return return
return super().delete(using=using, keep_parents=keep_parents) return super().delete(using=using, keep_parents=keep_parents)

View File

@ -4,14 +4,12 @@
import logging import logging
from functools import reduce
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from common.utils import signer from common.utils import signer
from .base import AssetUser from .base import BaseUser
from .asset import Asset from .asset import Asset
@ -19,7 +17,7 @@ __all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AdminUser(AssetUser): class AdminUser(BaseUser):
""" """
A privileged user that ansible can use it to push system user and so on A privileged user that ansible can use it to push system user and so on
""" """
@ -87,7 +85,7 @@ class AdminUser(AssetUser):
continue continue
class SystemUser(AssetUser): class SystemUser(BaseUser):
PROTOCOL_SSH = 'ssh' PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp' PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet' PROTOCOL_TELNET = 'telnet'
@ -107,9 +105,11 @@ class SystemUser(AssetUser):
(LOGIN_AUTO, _('Automatic login')), (LOGIN_AUTO, _('Automatic login')),
(LOGIN_MANUAL, _('Manually login')) (LOGIN_MANUAL, _('Manually login'))
) )
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)]) priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
@ -117,9 +117,20 @@ class SystemUser(AssetUser):
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
_prefer = 'system_user'
def __str__(self): def __str__(self):
return '{0.name}({0.username})'.format(self) username = self.username
if self.username_same_with_user:
username = 'dynamic'
return '{0.name}({1})'.format(self, username)
def get_username(self):
if self.username_same_with_user:
return list(self.users.values_list('username', flat=True))
else:
return self.username
@property @property
def nodes_amount(self): def nodes_amount(self):
@ -147,6 +158,11 @@ class SystemUser(AssetUser):
def can_perm_to_asset(self): def can_perm_to_asset(self):
return self.protocol not in [self.PROTOCOL_MYSQL] return self.protocol not in [self.PROTOCOL_MYSQL]
def _merge_auth(self, other):
super()._merge_auth(other)
if self.username_same_with_user:
self.username = other.username
@property @property
def cmd_filter_rules(self): def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule from .cmd_filter import CommandFilterRule

View File

@ -55,3 +55,11 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
class TaskIDSerializer(serializers.Serializer): class TaskIDSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True) task = serializers.CharField(read_only=True)
class AssetUserTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
task = serializers.CharField(read_only=True)

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re
from rest_framework import serializers from rest_framework import serializers
from django.db.models import Prefetch from django.db.models import Prefetch, F
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@ -12,8 +12,9 @@ from .base import ConnectivitySerializer
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'AssetSerializer', 'AssetSimpleSerializer',
'AssetDisplaySerializer',
'ProtocolsField', 'PlatformSerializer', 'ProtocolsField', 'PlatformSerializer',
'AssetDetailSerializer', 'AssetDetailSerializer', 'AssetTaskSerializer',
] ]
@ -66,8 +67,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform") slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
) )
protocols = ProtocolsField(label=_('Protocols'), required=False) protocols = ProtocolsField(label=_('Protocols'), required=False)
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
""" """
资产的数据结构 资产的数据结构
""" """
@ -81,7 +80,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
'hostname_raw', 'comment', 'created_by', 'date_created', 'hostname_raw', 'comment', 'created_by', 'date_created',
'hardware_info', 'connectivity', 'hardware_info',
] ]
read_only_fields = ( read_only_fields = (
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
@ -102,7 +101,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(
Prefetch('nodes', queryset=Node.objects.all().only('id')), Prefetch('nodes', queryset=Node.objects.all().only('id')),
Prefetch('labels', queryset=Label.objects.all().only('id')), Prefetch('labels', queryset=Label.objects.all().only('id')),
).select_related('admin_user', 'domain', 'platform') ).select_related('admin_user', 'domain', 'platform') \
.annotate(platform_base=F('platform__base'))
return queryset return queryset
def compatible_with_old_protocol(self, validated_data): def compatible_with_old_protocol(self, validated_data):
@ -130,6 +130,28 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class AssetDisplaySerializer(AssetSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta(AssetSerializer.Meta):
fields = [
'id', 'ip', 'hostname', 'protocol', 'port',
'protocols', 'is_active', 'public_ip',
'number', 'vendor', 'model', 'sn',
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
'hostname_raw', 'comment', 'created_by', 'date_created',
'hardware_info', 'connectivity',
]
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset\
.annotate(admin_user_username=F('admin_user__username'))
return queryset
class PlatformSerializer(serializers.ModelSerializer): class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True) meta = serializers.DictField(required=False, allow_null=True)
@ -151,3 +173,12 @@ class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
fields = ['id', 'hostname', 'ip', 'connectivity', 'port'] fields = ['id', 'hostname', 'ip', 'connectivity', 'port']
class AssetTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('refresh', 'refresh'),
('test', 'test'),
)
task = serializers.CharField(read_only=True)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)

View File

@ -8,39 +8,23 @@ from common.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AuthBook, Asset from ..models import AuthBook, Asset
from ..backends import AssetUserManager from ..backends import AssetUserManager
from .base import ConnectivitySerializer, AuthSerializerMixin from .base import ConnectivitySerializer, AuthSerializerMixin
__all__ = [ __all__ = [
'AssetUserSerializer', 'AssetUserAuthInfoSerializer', 'AssetUserWriteSerializer', 'AssetUserReadSerializer',
'AssetUserExportSerializer', 'AssetUserPushSerializer', 'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer',
] ]
class BasicAssetSerializer(serializers.ModelSerializer): class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = Asset
fields = ['hostname', 'ip']
class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
ip = serializers.CharField(read_only=True, label=_("IP"))
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
backend = serializers.CharField(read_only=True, label=_("Backend"))
class Meta: class Meta:
model = AuthBook model = AuthBook
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
read_only_fields = (
'date_created', 'date_updated', 'created_by',
'is_latest', 'version', 'connectivity',
)
fields = [ fields = [
"id", "hostname", "ip", "username", "password", "asset", "version", 'id', 'username', 'password', 'private_key', "public_key",
"is_latest", "connectivity", "backend", 'asset', 'comment',
"date_created", "date_updated", "private_key", "public_key",
] ]
extra_kwargs = { extra_kwargs = {
'username': {'required': True}, 'username': {'required': True},
@ -57,7 +41,32 @@ class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return instance return instance
class AssetUserExportSerializer(AssetUserSerializer): class AssetUserReadSerializer(AssetUserWriteSerializer):
id = serializers.CharField(read_only=True, source='union_id', label=_("ID"))
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
ip = serializers.CharField(read_only=True, label=_("IP"))
asset = serializers.CharField(source='asset_id', label=_('Asset'))
backend = serializers.CharField(read_only=True, label=_("Backend"))
class Meta(AssetUserWriteSerializer.Meta):
read_only_fields = (
'date_created', 'date_updated',
'created_by', 'version',
)
fields = [
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'hostname', 'ip', 'backend', 'version',
'date_created', "date_updated", 'comment',
]
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
class AssetUserAuthInfoSerializer(AssetUserReadSerializer):
password = serializers.CharField( password = serializers.CharField(
max_length=256, allow_blank=True, allow_null=True, max_length=256, allow_blank=True, allow_null=True,
required=False, label=_('Password') required=False, label=_('Password')
@ -72,12 +81,6 @@ class AssetUserExportSerializer(AssetUserSerializer):
) )
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
class Meta:
model = AuthBook
fields = ['password', 'private_key', 'public_key']
class AssetUserPushSerializer(serializers.Serializer): class AssetUserPushSerializer(serializers.Serializer):
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset")) asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
username = serializers.CharField(max_length=1024) username = serializers.CharField(max_length=1024)

View File

@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_pubkey_gen, validate_ssh_private_key from common.utils import ssh_pubkey_gen, validate_ssh_private_key
from ..models import AssetUser
class AuthSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.ModelSerializer):
@ -60,9 +61,6 @@ class AuthSerializerMixin:
if not value: if not value:
validated_data.pop(field, None) validated_data.pop(field, None)
# print(validated_data)
# raise serializers.ValidationError(">>>>>>")
def create(self, validated_data): def create(self, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
return super().create(validated_data) return super().create(validated_data)
@ -70,3 +68,15 @@ class AuthSerializerMixin:
def update(self, instance, validated_data): def update(self, instance, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
return super().update(instance, validated_data) return super().update(instance, validated_data)
class AuthInfoSerializer(serializers.ModelSerializer):
private_key = serializers.ReadOnlyField(source='get_private_key')
class Meta:
model = AssetUser
fields = [
'username', 'password',
'private_key', 'public_key',
'date_updated',
]

View File

@ -8,7 +8,7 @@ from ..models import Asset, Node
__all__ = [ __all__ = [
'NodeSerializer', "NodeAddChildrenSerializer", 'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer", "NodeAssetsSerializer", "NodeTaskSerializer",
] ]
@ -51,3 +51,12 @@ class NodeAssetsSerializer(BulkOrgResourceModelSerializer):
class NodeAddChildrenSerializer(serializers.Serializer): class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField() nodes = serializers.ListField()
class NodeTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('refresh', 'refresh'),
('test', 'test'),
('refresh_cache', 'refresh_cache'),
)
task = serializers.CharField(read_only=True)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)

View File

@ -1,20 +1,20 @@
import re
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Node from assets.models import Node
from ..models import SystemUser from ..models import SystemUser, Asset
from .base import AuthSerializer, AuthSerializerMixin from .base import AuthSerializerMixin
__all__ = [ __all__ = [
'SystemUserSerializer', 'SystemUserAuthSerializer', 'SystemUserSerializer', 'SystemUserListSerializer',
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
] ]
@ -28,10 +28,13 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
model = SystemUser model = SystemUser
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [ fields = [
'id', 'name', 'username', 'password', 'public_key', 'private_key', 'id', 'name', 'username', 'protocol',
'login_mode', 'login_mode_display', 'priority', 'protocol', 'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', 'username_same_with_user',
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
'assets_amount', 'nodes_amount', 'auto_generate_key' 'auto_generate_key', 'sftp_root',
'assets_amount',
] ]
extra_kwargs = { extra_kwargs = {
'password': {"write_only": True}, 'password': {"write_only": True},
@ -67,17 +70,43 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
value = False value = False
return value return value
def validate_username_same_with_user(self, username_same_with_user):
if not username_same_with_user:
return username_same_with_user
protocol = self.initial_data.get("protocol", "ssh")
queryset = SystemUser.objects.filter(
protocol=protocol, username_same_with_user=True
)
if self.instance:
queryset = queryset.exclude(id=self.instance.id)
exists = queryset.exists()
if not exists:
return username_same_with_user
error = _("Username same with user with protocol {} only allow 1").format(protocol)
raise serializers.ValidationError(error)
def validate_username(self, username): def validate_username(self, username):
if username: if username:
return username return username
login_mode = self.initial_data.get("login_mode") login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol") protocol = self.initial_data.get("protocol")
username_same_with_user = self.initial_data.get("username_same_with_user")
if username_same_with_user:
return ''
if login_mode == SystemUser.LOGIN_AUTO and \ if login_mode == SystemUser.LOGIN_AUTO and \
protocol != SystemUser.PROTOCOL_VNC: protocol != SystemUser.PROTOCOL_VNC:
msg = _('* Automatic login mode must fill in the username.') msg = _('* Automatic login mode must fill in the username.')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
return username return username
def validate_sftp_root(self, value):
if value in ['home', 'tmp']:
return value
if not value.startswith('/'):
error = _("Path should starts with /")
raise serializers.ValidationError(error)
return value
def validate_password(self, password): def validate_password(self, password):
super().validate_password(password) super().validate_password(password)
auto_gen_key = self.initial_data.get("auto_generate_key", False) auto_gen_key = self.initial_data.get("auto_generate_key", False)
@ -112,29 +141,42 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
attrs["public_key"] = public_key attrs["public_key"] = public_key
return attrs return attrs
class SystemUserListSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields = [
'id', 'name', 'username', 'protocol',
'login_mode', 'login_mode_display',
'priority', "username_same_with_user",
'auto_push', 'sudo', 'shell', 'comment',
"assets_amount",
'auto_generate_key',
'sftp_root',
]
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('cmd_filters', 'nodes') queryset = queryset.annotate(assets_amount=Count("assets"))
return queryset return queryset
class SystemUserAuthSerializer(AuthSerializer): class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
""" class Meta(SystemUserSerializer.Meta):
系统用户认证信息
"""
private_key = serializers.SerializerMethodField()
class Meta:
model = SystemUser
fields = [ fields = [
"id", "name", "username", "protocol", 'id', 'name', 'username', 'protocol',
"login_mode", "password", "private_key", 'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', 'username_same_with_user',
'auto_push', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root',
] ]
extra_kwargs = {
@staticmethod 'nodes_amount': {'label': _('Node')},
def get_private_key(obj): 'assets_amount': {'label': _('Asset')},
return obj.get_private_key() 'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
}
class SystemUserSimpleSerializer(serializers.ModelSerializer): class SystemUserSimpleSerializer(serializers.ModelSerializer):
@ -186,3 +228,25 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
return self.tree.get_node_full_tag(obj.node_key) return self.tree.get_node_full_tag(obj.node_key)
else: else:
return obj.node.full_value return obj.node.full_value
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
model = SystemUser.users.through
fields = [
'id', "user", "user_display",
]
class SystemUserTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
("test", "test"),
("push", "push"),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
asset = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, allow_null=True, required=False, write_only=True
)
task = serializers.CharField(read_only=True)

View File

@ -7,14 +7,17 @@ from django.db.models.signals import (
from django.db.models.aggregates import Count from django.db.models.aggregates import Count
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger, timeit from common.utils import get_logger
from common.decorator import on_transaction_commit from common.decorator import on_transaction_commit
from orgs.utils import tmp_to_root_org
from .models import Asset, SystemUser, Node, AuthBook from .models import Asset, SystemUser, Node, AuthBook
from .utils import TreeService from .utils import TreeService
from .tasks import ( from .tasks import (
update_assets_hardware_info_util, update_assets_hardware_info_util,
test_asset_connectivity_util, test_asset_connectivity_util,
push_system_user_to_assets, push_system_user_to_assets,
push_system_user_to_assets_manual,
push_system_user_to_assets,
add_nodes_assets_to_system_users add_nodes_assets_to_system_users
) )
@ -94,6 +97,25 @@ def on_system_user_assets_change(sender, instance=None, action='', model=None, p
push_system_user_to_assets.delay(system_user, assets) push_system_user_to_assets.delay(system_user, assets)
@receiver(m2m_changed, sender=SystemUser.users.through)
def on_system_user_users_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs):
"""
当系统用户和用户关系发生变化时应该重新推送系统用户资产中
"""
if action != "post_add":
return
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == SystemUser:
system_users = queryset
else:
system_users = [instance]
for s in system_users:
push_system_user_to_assets_manual.delay(s)
@receiver(m2m_changed, sender=SystemUser.nodes.through) @receiver(m2m_changed, sender=SystemUser.nodes.through)
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs): def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
""" """
@ -113,6 +135,20 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None,
add_nodes_assets_to_system_users.delay(nodes_keys, system_users) add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
@receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(sender, instance=None, action=None, model=None,
pk_set=None, reverse=False, **kwargs):
"""
当系统用户和用户组关系发生变化时应该将组下用户关联到新的系统用户上
"""
if action != "post_add" or reverse:
return
logger.info("System user groups update signal recv: {}".format(instance))
groups = model.objects.filter(pk__in=pk_set).annotate(users_count=Count("users"))
users = groups.filter(users_count__gt=0).values_list('users', flat=True)
instance.users.add(*tuple(users))
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_change(sender, instance=None, action='', **kwargs): def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
""" """
@ -121,6 +157,8 @@ def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
if action.startswith('post'): if action.startswith('post'):
logger.debug("Asset nodes change signal recv: {}".format(instance)) logger.debug("Asset nodes change signal recv: {}".format(instance))
Node.refresh_assets() Node.refresh_assets()
with tmp_to_root_org():
Node.refresh_assets()
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
@ -195,6 +233,8 @@ def on_asset_nodes_remove(sender, instance=None, action='', model=None,
def on_node_update_or_created(sender, **kwargs): def on_node_update_or_created(sender, **kwargs):
# 刷新节点 # 刷新节点
Node.refresh_nodes() Node.refresh_nodes()
with tmp_to_root_org():
Node.refresh_nodes()
@receiver(post_save, sender=AuthBook) @receiver(post_save, sender=AuthBook)

View File

@ -4,11 +4,12 @@ from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.cache import cache from django.core.cache import cache
from orgs.utils import tmp_to_root_org, org_aware_func
from common.utils import get_logger from common.utils import get_logger
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import register_as_period_task
from ..models import AdminUser from ..models import AdminUser
from .utils import clean_hosts from .utils import clean_ansible_task_hosts
from .asset_connectivity import test_asset_connectivity_util from .asset_connectivity import test_asset_connectivity_util
from . import const from . import const
@ -20,7 +21,7 @@ __all__ = [
] ]
@shared_task(queue="ansible") @org_aware_func("admin_user")
def test_admin_user_connectivity_util(admin_user, task_name): def test_admin_user_connectivity_util(admin_user, task_name):
""" """
Test asset admin user can connect or not. Using ansible api do that Test asset admin user can connect or not. Using ansible api do that
@ -29,7 +30,7 @@ def test_admin_user_connectivity_util(admin_user, task_name):
:return: :return:
""" """
assets = admin_user.get_related_assets() assets = admin_user.get_related_assets()
hosts = clean_hosts(assets) hosts = clean_ansible_task_hosts(assets)
if not hosts: if not hosts:
return {} return {}
summary = test_asset_connectivity_util(hosts, task_name) summary = test_asset_connectivity_util(hosts, task_name)
@ -51,9 +52,12 @@ def test_admin_user_connectivity_period():
logger.debug("Test admin user connectivity, less than 40 minutes, skip") logger.debug("Test admin user connectivity, less than 40 minutes, skip")
return return
cache.set(key, 1, 60*40) cache.set(key, 1, 60*40)
with tmp_to_root_org():
admin_users = AdminUser.objects.all() admin_users = AdminUser.objects.all()
for admin_user in admin_users: for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(admin_user.name) task_name = _("Test admin user connectivity period: {}").format(
admin_user.name
)
test_admin_user_connectivity_util(admin_user, task_name) test_admin_user_connectivity_util(admin_user, task_name)
cache.set(key, 1, 60*40) cache.set(key, 1, 60*40)

View File

@ -1,55 +1,55 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from itertools import groupby
from collections import defaultdict from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import org_aware_func
from ..models.utils import Connectivity from ..models.utils import Connectivity
from . import const from . import const
from .utils import clean_hosts from .utils import clean_ansible_task_hosts, group_asset_by_platform
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = ['test_asset_connectivity_util', 'test_asset_connectivity_manual'] __all__ = [
'test_asset_connectivity_util', 'test_asset_connectivity_manual',
'test_node_assets_connectivity_manual',
]
@shared_task(queue="ansible") @shared_task(queue="ansible")
@org_aware_func("assets")
def test_asset_connectivity_util(assets, task_name=None): def test_asset_connectivity_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
task_name = _("Test assets connectivity") task_name = _("Test assets connectivity")
hosts = clean_hosts(assets) hosts = clean_ansible_task_hosts(assets)
if not hosts: if not hosts:
return {} return {}
platform_hosts_map = {}
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
for i in platform_hosts:
platform_hosts_map[i[0]] = list(i[1])
hosts_category = { platform_tasks_map = {
'linux': { "unixlike": const.PING_UNIXLIKE_TASKS,
'hosts': [], "windows": const.PING_WINDOWS_TASKS
'tasks': const.TEST_ADMIN_USER_CONN_TASKS
},
'windows': {
'hosts': [],
'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS
} }
}
for host in hosts:
hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \
else hosts_category['linux']['hosts']
hosts_list.append(host)
results_summary = dict( results_summary = dict(
contacted=defaultdict(dict), dark=defaultdict(dict), success=True contacted=defaultdict(dict), dark=defaultdict(dict), success=True
) )
created_by = assets[0].org_id for platform, _hosts in platform_hosts_map.items():
for k, value in hosts_category.items(): if not _hosts:
if not value['hosts']:
continue continue
logger.debug("System user not has special auth")
tasks = platform_tasks_map.get(platform)
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], task_name=task_name, hosts=_hosts, tasks=tasks,
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
created_by=created_by,
) )
raw, summary = task.run() raw, summary = task.run()
success = summary.get('success', False) success = summary.get('success', False)
@ -59,6 +59,7 @@ def test_asset_connectivity_util(assets, task_name=None):
results_summary['success'] &= success results_summary['success'] &= success
results_summary['contacted'].update(contacted) results_summary['contacted'].update(contacted)
results_summary['dark'].update(dark) results_summary['dark'].update(dark)
continue
for asset in assets: for asset in assets:
if asset.hostname in results_summary.get('dark', {}).keys(): if asset.hostname in results_summary.get('dark', {}).keys():
@ -79,3 +80,12 @@ def test_asset_connectivity_manual(asset):
return False, summary['dark'] return False, summary['dark']
else: else:
return True, "" return True, ""
@shared_task(queue="ansible")
def test_node_assets_connectivity_manual(node):
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
assets = node.get_all_assets()
result = test_asset_connectivity_util(assets, task_name=task_name)
return result

View File

@ -3,7 +3,9 @@
from celery import shared_task from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import get_logger from common.utils import get_logger, get_object_or_none
from orgs.utils import org_aware_func
from ..models import Asset
from . import const from . import const
from .utils import check_asset_can_run_ansible from .utils import check_asset_can_run_ansible
@ -13,15 +15,16 @@ logger = get_logger(__file__)
__all__ = [ __all__ = [
'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual', 'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual',
'get_test_asset_user_connectivity_tasks', 'get_test_asset_user_connectivity_tasks', 'test_user_connectivity',
'run_adhoc',
] ]
def get_test_asset_user_connectivity_tasks(asset): def get_test_asset_user_connectivity_tasks(asset):
if asset.is_unixlike(): if asset.is_unixlike():
tasks = const.TEST_ASSET_USER_CONN_TASKS tasks = const.PING_UNIXLIKE_TASKS
elif asset.is_windows(): elif asset.is_windows():
tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS tasks = const.PING_WINDOWS_TASKS
else: else:
msg = _( msg = _(
"The asset {} system platform {} does not " "The asset {} system platform {} does not "
@ -32,46 +35,98 @@ def get_test_asset_user_connectivity_tasks(asset):
return tasks return tasks
@shared_task(queue="ansible") def run_adhoc(task_name, tasks, inventory):
def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): """
:param task_name
:param tasks
:param inventory
"""
from ops.ansible.runner import AdHocRunner
runner = AdHocRunner(inventory, options=const.TASK_OPTIONS)
result = runner.run(tasks, 'all', task_name)
return result.results_raw, result.results_summary
def test_user_connectivity(task_name, asset, username, password=None, private_key=None):
"""
:param task_name
:param asset
:param username
:param password
:param private_key
"""
from ops.inventory import JMSCustomInventory
tasks = get_test_asset_user_connectivity_tasks(asset)
if not tasks:
logger.debug("No tasks ")
return {}, {}
inventory = JMSCustomInventory(
assets=[asset], username=username, password=password,
private_key=private_key
)
raw, summary = run_adhoc(
task_name=task_name, tasks=tasks, inventory=inventory
)
return raw, summary
@org_aware_func("asset_user")
def test_asset_user_connectivity_util(asset_user, task_name):
""" """
:param asset_user: <AuthBook>对象 :param asset_user: <AuthBook>对象
:param task_name: :param task_name:
:param run_as_admin:
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task
if not check_asset_can_run_ansible(asset_user.asset): if not check_asset_can_run_ansible(asset_user.asset):
return return
tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) try:
if not tasks: raw, summary = test_user_connectivity(
logger.debug("No tasks ") task_name=task_name, asset=asset_user.asset,
username=asset_user.username, password=asset_user.password,
private_key=asset_user.private_key
)
except Exception as e:
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
return return
args = (task_name,)
kwargs = {
'hosts': [asset_user.asset], 'tasks': tasks,
'pattern': 'all', 'options': const.TASK_OPTIONS,
'created_by': asset_user.org_id,
}
if run_as_admin:
kwargs["run_as_admin"] = True
else:
kwargs["run_as"] = asset_user.username
task, created = update_or_create_ansible_task(*args, **kwargs)
raw, summary = task.run()
asset_user.set_connectivity(summary) asset_user.set_connectivity(summary)
@shared_task(queue="ansible") @shared_task(queue="ansible")
def test_asset_users_connectivity_manual(asset_users, run_as_admin=False): def test_asset_users_connectivity_manual(asset_users):
""" """
:param asset_users: <AuthBook>对象 :param asset_users: <AuthBook>对象
""" """
for asset_user in asset_users: for asset_user in asset_users:
task_name = _("Test asset user connectivity: {}").format(asset_user) task_name = _("Test asset user connectivity: {}").format(asset_user)
test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin) test_asset_user_connectivity_util(asset_user, task_name)
@shared_task(queue="ansible")
def push_asset_user_util(asset_user):
"""
:param asset_user: <Asset user>对象
"""
from .push_system_user import push_system_user_util
if not asset_user.backend.startswith('system_user'):
logger.error("Asset user is not from system user")
return
union_id = asset_user.union_id
union_id_list = union_id.split('_')
if len(union_id_list) < 2:
logger.error("Asset user union id length less than 2")
return
system_user_id = union_id_list[0]
asset_id = union_id_list[1]
asset = get_object_or_none(Asset, pk=asset_id)
system_user = None
if not asset:
return
hosts = check_asset_can_run_ansible([asset])
if asset.is_unixlike:
pass

View File

@ -18,27 +18,10 @@ UPDATE_ASSETS_HARDWARE_TASKS = [
} }
] ]
TEST_ADMIN_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "ping",
}
}
]
TEST_WINDOWS_ADMIN_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "win_ping",
}
}
]
ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}" ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}"
SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}" SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}"
TEST_SYSTEM_USER_CONN_TASKS = [ PING_UNIXLIKE_TASKS = [
{ {
"name": "ping", "name": "ping",
"action": { "action": {
@ -46,7 +29,7 @@ TEST_SYSTEM_USER_CONN_TASKS = [
} }
} }
] ]
TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [ PING_WINDOWS_TASKS = [
{ {
"name": "ping", "name": "ping",
"action": { "action": {
@ -55,24 +38,6 @@ TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [
} }
] ]
TEST_ASSET_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "ping",
}
}
]
TEST_WINDOWS_ASSET_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "win_ping",
}
}
]
TASK_OPTIONS = { TASK_OPTIONS = {
'timeout': 10, 'timeout': 10,
'forks': 10, 'forks': 10,
@ -98,7 +63,9 @@ GATHER_ASSET_USERS_TASKS = [
"name": "get last login", "name": "get last login",
"action": { "action": {
"module": "shell", "module": "shell",
"args": "users=$(getent passwd | grep -v 'nologin' | grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done" "args": "users=$(getent passwd | grep -v 'nologin' | "
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | "
"head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
} }
} }
] ]

View File

@ -9,15 +9,16 @@ from django.utils.translation import ugettext as _
from common.utils import ( from common.utils import (
capacity_convert, sum_capacity, get_logger capacity_convert, sum_capacity, get_logger
) )
from orgs.utils import org_aware_func
from . import const from . import const
from .utils import clean_hosts from .utils import clean_ansible_task_hosts
logger = get_logger(__file__) logger = get_logger(__file__)
disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv')
__all__ = [ __all__ = [
'update_assets_hardware_info_util', 'update_asset_hardware_info_manual', 'update_assets_hardware_info_util', 'update_asset_hardware_info_manual',
'update_assets_hardware_info_period', 'update_assets_hardware_info_period', 'update_node_assets_hardware_info_manual',
] ]
@ -82,6 +83,7 @@ def set_assets_hardware_info(assets, result, **kwargs):
@shared_task @shared_task
@org_aware_func("assets")
def update_assets_hardware_info_util(assets, task_name=None): def update_assets_hardware_info_util(assets, task_name=None):
""" """
Using ansible api to update asset hardware info Using ansible api to update asset hardware info
@ -93,13 +95,13 @@ def update_assets_hardware_info_util(assets, task_name=None):
if task_name is None: if task_name is None:
task_name = _("Update some assets hardware info") task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hosts = clean_hosts(assets) hosts = clean_ansible_task_hosts(assets)
if not hosts: if not hosts:
return {} return {}
created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, created_by=created_by, task_name, hosts=hosts, tasks=tasks,
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, pattern='all', options=const.TASK_OPTIONS,
run_as_admin=True,
) )
result = task.run() result = task.run()
set_assets_hardware_info(assets, result) set_assets_hardware_info(assets, result)
@ -109,9 +111,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task(queue="ansible") @shared_task(queue="ansible")
def update_asset_hardware_info_manual(asset): def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname) task_name = _("Update asset hardware info: {}").format(asset.hostname)
update_assets_hardware_info_util( update_assets_hardware_info_util([asset], task_name=task_name)
[asset], task_name=task_name
)
@shared_task(queue="ansible") @shared_task(queue="ansible")
@ -123,3 +123,11 @@ def update_assets_hardware_info_period():
if not const.PERIOD_TASK_ENABLED: if not const.PERIOD_TASK_ENABLED:
logger.debug("Period task disabled, update assets hardware info pass") logger.debug("Period task disabled, update assets hardware info pass")
return return
@shared_task(queue="ansible")
def update_node_assets_hardware_info_manual(node):
task_name = _("Update node asset hardware information: {}").format(node.name)
assets = node.get_all_assets()
result = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return result

View File

@ -7,10 +7,10 @@ from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils import timezone from django.utils import timezone
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org, org_aware_func
from common.utils import get_logger from common.utils import get_logger
from ..models import GatheredUser, Node from ..models import GatheredUser, Node
from .utils import clean_hosts from .utils import clean_ansible_task_hosts
from . import const from . import const
__all__ = ['gather_asset_users', 'gather_nodes_asset_users'] __all__ = ['gather_asset_users', 'gather_nodes_asset_users']
@ -101,11 +101,12 @@ def add_asset_users(assets, results):
@shared_task(queue="ansible") @shared_task(queue="ansible")
@org_aware_func("assets")
def gather_asset_users(assets, task_name=None): def gather_asset_users(assets, task_name=None):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
task_name = _("Gather assets users") task_name = _("Gather assets users")
assets = clean_hosts(assets) assets = clean_ansible_task_hosts(assets)
if not assets: if not assets:
return return
hosts_category = { hosts_category = {
@ -131,7 +132,7 @@ def gather_asset_users(assets, task_name=None):
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'], task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'],
pattern='all', options=const.TASK_OPTIONS, pattern='all', options=const.TASK_OPTIONS,
run_as_admin=True, created_by=value['hosts'][0].org_id, run_as_admin=True,
) )
raw, summary = task.run() raw, summary = task.run()
results[k].update(raw['ok']) results[k].update(raw['ok'])

View File

@ -1,11 +1,13 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from itertools import groupby
from celery import shared_task from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import encrypt_password, get_logger from common.utils import encrypt_password, get_logger
from orgs.utils import tmp_to_org, org_aware_func
from . import const from . import const
from .utils import clean_hosts_by_protocol, clean_hosts from .utils import clean_ansible_task_hosts, group_asset_by_platform
logger = get_logger(__file__) logger = get_logger(__file__)
@ -15,31 +17,34 @@ __all__ = [
] ]
def get_push_linux_system_user_tasks(system_user): def get_push_unixlike_system_user_tasks(system_user, username=None):
if username is None:
username = system_user.username
password = system_user.password
public_key = system_user.public_key
tasks = [ tasks = [
{ {
'name': 'Add user {}'.format(system_user.username), 'name': 'Add user {}'.format(username),
'action': { 'action': {
'module': 'user', 'module': 'user',
'args': 'name={} shell={} state=present'.format( 'args': 'name={} shell={} state=present'.format(
system_user.username, system_user.shell, username, system_user.shell or '/bin/bash',
), ),
} }
}, },
{ {
'name': 'Add group {}'.format(system_user.username), 'name': 'Add group {}'.format(username),
'action': { 'action': {
'module': 'group', 'module': 'group',
'args': 'name={} state=present'.format( 'args': 'name={} state=present'.format(username),
system_user.username,
),
} }
}, },
{ {
'name': 'Check home dir exists', 'name': 'Check home dir exists',
'action': { 'action': {
'module': 'stat', 'module': 'stat',
'args': 'path=/home/{}'.format(system_user.username) 'args': 'path=/home/{}'.format(username)
}, },
'register': 'home_existed' 'register': 'home_existed'
}, },
@ -47,29 +52,29 @@ def get_push_linux_system_user_tasks(system_user):
'name': "Set home dir permission", 'name': "Set home dir permission",
'action': { 'action': {
'module': 'file', 'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username) 'args': "path=/home/{0} owner={0} group={0} mode=700".format(username)
}, },
'when': 'home_existed.stat.exists == true' 'when': 'home_existed.stat.exists == true'
} }
] ]
if system_user.password: if password:
tasks.append({ tasks.append({
'name': 'Set {} password'.format(system_user.username), 'name': 'Set {} password'.format(username),
'action': { 'action': {
'module': 'user', 'module': 'user',
'args': 'name={} shell={} state=present password={}'.format( 'args': 'name={} shell={} state=present password={}'.format(
system_user.username, system_user.shell, username, system_user.shell,
encrypt_password(system_user.password, salt="K3mIlKK"), encrypt_password(password, salt="K3mIlKK"),
), ),
} }
}) })
if system_user.public_key: if public_key:
tasks.append({ tasks.append({
'name': 'Set {} authorized key'.format(system_user.username), 'name': 'Set {} authorized key'.format(username),
'action': { 'action': {
'module': 'authorized_key', 'module': 'authorized_key',
'args': "user={} state=present key='{}'".format( 'args': "user={} state=present key='{}'".format(
system_user.username, system_user.public_key username, public_key
) )
} }
}) })
@ -81,26 +86,27 @@ def get_push_linux_system_user_tasks(system_user):
sudo_tmp.append(s.strip(',')) sudo_tmp.append(s.strip(','))
sudo = ','.join(sudo_tmp) sudo = ','.join(sudo_tmp)
tasks.append({ tasks.append({
'name': 'Set {} sudo setting'.format(system_user.username), 'name': 'Set {} sudo setting'.format(username),
'action': { 'action': {
'module': 'lineinfile', 'module': 'lineinfile',
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
"line='{0} ALL=(ALL) NOPASSWD: {1}' " "line='{0} ALL=(ALL) NOPASSWD: {1}' "
"validate='visudo -cf %s'".format( "validate='visudo -cf %s'".format(username, sudo)
system_user.username, sudo,
)
} }
}) })
return tasks return tasks
def get_push_windows_system_user_tasks(system_user): def get_push_windows_system_user_tasks(system_user, username=None):
if username is None:
username = system_user.username
password = system_user.password
tasks = [] tasks = []
if not system_user.password: if not password:
return tasks return tasks
tasks.append({ task = {
'name': 'Add user {}'.format(system_user.username), 'name': 'Add user {}'.format(username),
'action': { 'action': {
'module': 'win_user', 'module': 'win_user',
'args': 'fullname={} ' 'args': 'fullname={} '
@ -112,84 +118,100 @@ def get_push_windows_system_user_tasks(system_user):
'password_never_expires=yes ' 'password_never_expires=yes '
'groups="Users,Remote Desktop Users" ' 'groups="Users,Remote Desktop Users" '
'groups_action=add ' 'groups_action=add '
''.format(system_user.name, ''.format(username, username, password),
system_user.username,
system_user.password),
} }
}) }
tasks.append(task)
return tasks return tasks
def get_push_system_user_tasks(host, system_user): def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
if host.is_unixlike(): """
tasks = get_push_linux_system_user_tasks(system_user) :param system_user:
elif host.is_windows(): :param platform:
tasks = get_push_windows_system_user_tasks(system_user) :param username: 当动态时近推送某个
else: :return:
msg = _( """
"The asset {} system platform {} does not " get_task_map = {
"support run Ansible tasks".format(host.hostname, host.platform) "unixlike": get_push_unixlike_system_user_tasks,
) "windows": get_push_windows_system_user_tasks,
logger.info(msg) }
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
if not system_user.username_same_with_user:
return get_tasks(system_user)
tasks = [] tasks = []
# 仅推送这个username
if username is not None:
tasks.extend(get_tasks(system_user, username))
return tasks
users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users)))
for _username in users:
tasks.extend(get_tasks(system_user, _username))
return tasks return tasks
@shared_task(queue="ansible") @org_aware_func("system_user")
def push_system_user_util(system_user, assets, task_name): def push_system_user_util(system_user, assets, task_name, username=None):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if not system_user.is_need_push(): hosts = clean_ansible_task_hosts(assets, system_user=system_user)
msg = _("Push system user task skip, auto push not enable or "
"protocol is not ssh or rdp: {}").format(system_user.name)
logger.info(msg)
return {}
# Set root as system user is dangerous
if system_user.username.lower() in ["root", "administrator"]:
msg = _("For security, do not push user {}".format(system_user.username))
logger.info(msg)
return {}
hosts = clean_hosts(assets)
if not hosts: if not hosts:
return {} return {}
hosts = clean_hosts_by_protocol(system_user, hosts) platform_hosts_map = {}
if not hosts: hosts_sorted = sorted(hosts, key=group_asset_by_platform)
return {} platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
for i in platform_hosts:
platform_hosts_map[i[0]] = list(i[1])
for host in hosts: def run_task(_tasks, _hosts):
system_user.load_specific_asset_auth(host) if not _tasks:
tasks = get_push_system_user_tasks(host, system_user) return
if not tasks:
continue
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=[host], tasks=tasks, pattern='all', task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id,
) )
task.run() task.run()
for platform, _hosts in platform_hosts_map.items():
if not _hosts:
continue
print(_("Start push system user for platform: [{}]").format(platform))
print(_("Hosts count: {}").format(len(_hosts)))
if not system_user.has_special_auth():
logger.debug("System user not has special auth")
tasks = get_push_system_user_tasks(system_user, platform, username=username)
run_task(tasks, _hosts)
continue
for _host in _hosts:
system_user.load_asset_special_auth(_host)
tasks = get_push_system_user_tasks(system_user, platform, username=username)
run_task(tasks, [_host])
@shared_task(queue="ansible") @shared_task(queue="ansible")
def push_system_user_to_assets_manual(system_user): def push_system_user_to_assets_manual(system_user, username=None):
assets = system_user.get_all_assets() assets = system_user.get_related_assets()
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name=task_name) return push_system_user_util(system_user, assets, task_name=task_name, username=username)
@shared_task(queue="ansible") @shared_task(queue="ansible")
def push_system_user_a_asset_manual(system_user, asset): def push_system_user_a_asset_manual(system_user, asset, username=None):
task_name = _("Push system users to asset: {} => {}").format( if username is None:
system_user.name, asset username = system_user.username
task_name = _("Push system users to asset: {}({}) => {}").format(
system_user.name, username, asset
) )
return push_system_user_util(system_user, [asset], task_name=task_name) return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
@shared_task(queue="ansible") @shared_task(queue="ansible")
def push_system_user_to_assets(system_user, assets): def push_system_user_to_assets(system_user, assets, username=None):
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name) return push_system_user_util(system_user, assets, task_name, username=username)

View File

@ -1,13 +1,17 @@
from itertools import groupby
from collections import defaultdict from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import tmp_to_org, org_aware_func
from ..models import SystemUser from ..models import SystemUser
from . import const from . import const
from .utils import clean_hosts, clean_hosts_by_protocol from .utils import (
clean_ansible_task_hosts, group_asset_by_platform
)
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
@ -16,7 +20,7 @@ __all__ = [
] ]
@shared_task(queue="ansible") @org_aware_func("system_user")
def test_system_user_connectivity_util(system_user, assets, task_name): def test_system_user_connectivity_util(system_user, assets, task_name):
""" """
Test system cant connect his assets or not. Test system cant connect his assets or not.
@ -27,41 +31,34 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
hosts = clean_hosts(assets) hosts = clean_ansible_task_hosts(assets, system_user=system_user)
if not hosts: if not hosts:
return {} return {}
platform_hosts_map = {}
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
for i in platform_hosts:
platform_hosts_map[i[0]] = list(i[1])
hosts = clean_hosts_by_protocol(system_user, hosts) platform_tasks_map = {
if not hosts: "unixlike": const.PING_UNIXLIKE_TASKS,
return {} "windows": const.PING_WINDOWS_TASKS
hosts_category = {
'linux': {
'hosts': [],
'tasks': const.TEST_SYSTEM_USER_CONN_TASKS
},
'windows': {
'hosts': [],
'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS
} }
}
for host in hosts:
hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \
else hosts_category['linux']['hosts']
hosts_list.append(host)
results_summary = dict( results_summary = dict(
contacted=defaultdict(dict), dark=defaultdict(dict), success=True contacted=defaultdict(dict), dark=defaultdict(dict), success=True
) )
for k, value in hosts_category.items():
if not value['hosts']: def run_task(_tasks, _hosts, _username):
continue old_name = "{}".format(system_user)
task, created = update_or_create_ansible_task( new_name = "{}({})".format(system_user.name, _username)
task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], _task_name = task_name.replace(old_name, new_name)
_task, created = update_or_create_ansible_task(
task_name=_task_name, hosts=_hosts, tasks=_tasks,
pattern='all', options=const.TASK_OPTIONS, pattern='all', options=const.TASK_OPTIONS,
run_as=system_user.username, created_by=system_user.org_id, run_as=_username,
) )
raw, summary = task.run() raw, summary = _task.run()
success = summary.get('success', False) success = summary.get('success', False)
contacted = summary.get('contacted', {}) contacted = summary.get('contacted', {})
dark = summary.get('dark', {}) dark = summary.get('dark', {})
@ -70,23 +67,45 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
results_summary['contacted'].update(contacted) results_summary['contacted'].update(contacted)
results_summary['dark'].update(dark) results_summary['dark'].update(dark)
for platform, _hosts in platform_hosts_map.items():
if not _hosts:
continue
if platform not in ["unixlike", "windows"]:
continue
tasks = platform_tasks_map[platform]
print(_("Start test system user connectivity for platform: [{}]").format(platform))
print(_("Hosts count: {}").format(len(_hosts)))
# 用户名不是动态的,用户名则是一个
if not system_user.username_same_with_user:
logger.debug("System user not has special auth")
run_task(tasks, _hosts, system_user.username)
# 否则需要多个任务
else:
users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users)))
for username in users:
run_task(tasks, _hosts, username)
system_user.set_connectivity(results_summary) system_user.set_connectivity(results_summary)
return results_summary return results_summary
@shared_task(queue="ansible") @shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_manual(system_user): def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectivity: {}").format(system_user) task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_all_assets() assets = system_user.get_related_assets()
return test_system_user_connectivity_util(system_user, assets, task_name) test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task(queue="ansible") @shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_a_asset(system_user, asset): def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectivity: {} => {}").format( task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset system_user, asset
) )
return test_system_user_connectivity_util(system_user, [asset], task_name) test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task(queue="ansible") @shared_task(queue="ansible")
@ -94,8 +113,9 @@ def test_system_user_connectivity_period():
if not const.PERIOD_TASK_ENABLED: if not const.PERIOD_TASK_ENABLED:
logger.debug("Period task disabled, test system user connectivity pass") logger.debug("Period task disabled, test system user connectivity pass")
return return
system_users = SystemUser.objects.all() queryset_map = SystemUser.objects.all_group_by_org()
for system_user in system_users: for org, system_user in queryset_map.items():
task_name = _("Test system user connectivity period: {}").format(system_user) task_name = _("Test system user connectivity period: {}").format(system_user)
assets = system_user.get_all_assets() with tmp_to_org(org):
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name) test_system_user_connectivity_util(system_user, assets, task_name)

View File

@ -7,7 +7,8 @@ from common.utils import get_logger
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'check_asset_can_run_ansible', 'clean_hosts', 'clean_hosts_by_protocol' 'check_asset_can_run_ansible', 'clean_ansible_task_hosts',
'group_asset_by_platform',
] ]
@ -23,23 +24,43 @@ def check_asset_can_run_ansible(asset):
return True return True
def clean_hosts(assets): def check_system_user_can_run_ansible(system_user):
clean_assets = [] if not system_user.is_need_push():
msg = _("Push system user task skip, auto push not enable or "
"protocol is not ssh or rdp: {}").format(system_user.name)
logger.info(msg)
return False
# Push root as system user is dangerous
if system_user.username.lower() in ["root", "administrator"]:
msg = _("For security, do not push user {}".format(system_user.username))
logger.info(msg)
return False
# if system_user.protocol != "ssh":
# msg = _("System user protocol not ssh: {}".format(system_user))
# logger.info(msg)
# return False
return True
def clean_ansible_task_hosts(assets, system_user=None):
if system_user and not check_system_user_can_run_ansible(system_user):
return []
cleaned_assets = []
for asset in assets: for asset in assets:
if not check_asset_can_run_ansible(asset): if not check_asset_can_run_ansible(asset):
continue continue
clean_assets.append(asset) cleaned_assets.append(asset)
if not clean_assets: if not cleaned_assets:
logger.info(_("No assets matched, stop task")) logger.info(_("No assets matched, stop task"))
return clean_assets return cleaned_assets
def clean_hosts_by_protocol(system_user, assets): def group_asset_by_platform(asset):
hosts = [ if asset.is_unixlike():
asset for asset in assets return 'unixlike'
if asset.has_protocol(system_user.protocol) elif asset.is_windows():
] return 'windows'
if not hosts: else:
msg = _("No assets matched related system user protocol, stop task") return 'other'
logger.info(msg)
return hosts

View File

@ -6,25 +6,25 @@
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_update_modal_confirm').trigger('click'); return false;}"> <form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_update_modal_confirm').trigger('click'); return false;}">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label">{% trans "Hostname" %}</label> <label class="col-sm-2 control-label">{% trans "Hostname" %}: </label>
<div class="col-sm-10"> <div class="col-sm-10">
<p class="form-control-static" id="id_hostname_p"></p> <p class="form-control-static" id="id_hostname_p"></p>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label">{% trans "Username" %}</label> <label class="col-sm-2 control-label">{% trans "Username" %}: </label>
<div class="col-sm-10"> <div class="col-sm-10">
<p class="form-control-static" id="id_username_p"></p> <p class="form-control-static" id="id_username_p"></p>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label">{% trans "Password" %}</label> <label class="col-sm-2 control-label">{% trans "Password" %}: </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input class="form-control" id="id_password_auth" type="password" name="password" placeholder="{% trans 'Please input password' %}"/> <input class="form-control" id="id_password_auth" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label">{% trans "Private key" %}</label> <label class="col-sm-2 control-label">{% trans "Private key" %}: </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="row bootstrap3-multi-input"> <div class="row bootstrap3-multi-input">
<div class="col-xs-12"> <div class="col-xs-12">

View File

@ -12,19 +12,19 @@
<form class="form-horizontal" action="" style="padding-top: 20px"> <form class="form-horizontal" action="" style="padding-top: 20px">
<div class="auth-field"> <div class="auth-field">
<div class="form-group"> <div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Hostname' %}</label> <label for="" class="col-sm-2 control-label">{% trans 'Hostname' %}:</label>
<div class="col-sm-8"> <div class="col-sm-8">
<p class="form-control-static" id="id_hostname_view"></p> <p class="form-control-static" id="id_hostname_view"></p>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Username' %}</label> <label for="" class="col-sm-2 control-label">{% trans 'Username' %}:</label>
<div class="col-sm-8" > <div class="col-sm-8" >
<p class="form-control-static" id="id_username_view"></p> <p class="form-control-static" id="id_username_view"></p>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Password' %}</label> <label for="" class="col-sm-2 control-label">{% trans 'Password' %}:</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input id="id_password_view" type="password" class="form-control" value="" readonly style="border: none;padding-left: 0;background-color: #fff;width: 100%"> <input id="id_password_view" type="password" class="form-control" value="" readonly style="border: none;padding-left: 0;background-color: #fff;width: 100%">
</div> </div>
@ -38,11 +38,11 @@
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script> <script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script> <script>
var showPassword = false; var showPassword = false;
var authAssetId = "";
var authHostname = ""; var authHostname = "";
var authUsername = ""; var authUsername = "";
var mfaFor = ""; var mfaFor = "";
var authUid = "";
var authInfoDetailUrl = "{% url "api-assets:asset-user-auth-info-detail" pk=DEFAULT_PK %}";
function initClipboard() { function initClipboard() {
var clipboard = new Clipboard('.btn-copy-password', { var clipboard = new Clipboard('.btn-copy-password', {
@ -56,12 +56,10 @@ function initClipboard() {
} }
function showAuth() { function showAuth() {
var url = "{% url "api-assets:asset-user-auth-info" %}?asset_id=" + authAssetId + "&username=" + authUsername; var url = authInfoDetailUrl.replace("{{ DEFAULT_PK }}", authUid);
if (prefer) {
url = setUrlParam(url, 'prefer', prefer)
}
$("#id_username_view").html(authUsername); $("#id_username_view").html(authUsername);
$("#id_hostname_view").html(authHostname); $("#id_hostname_view").html(authHostname);
$("#id_password_view").val('');
var success = function (data) { var success = function (data) {
var password = data.password; var password = data.password;
$("#id_password_view").val(password); $("#id_password_view").val(password);
@ -89,7 +87,13 @@ $(document).ready(function () {
$("#id_password_view").attr("type", "password") $("#id_password_view").attr("type", "password")
} }
}).on("show.bs.modal", "#asset_user_auth_view", function () { }).on("show.bs.modal", "#asset_user_auth_view", function () {
showPassword = false;
$("#id_password_view").attr("type", "password");
showAuth(); showAuth();
}).on("hide.bs.modal", "#asset_user_auth_view", function () {
$("#id_username_view").html('');
$("#id_hostname_view").html('');
$("#id_password_view").val('');
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -8,8 +8,8 @@
table.dataTable tbody tr.selected a { table.dataTable tbody tr.selected a {
color: rgb(103, 106, 108);; color: rgb(103, 106, 108);;
} }
</style> </style>
<table class="table table-striped table-bordered table-hover" id="asset_user_list_table" style="width: 100%"> <table class="table table-striped table-bordered table-hover" id="asset_user_list_table" style="width: 100%">
<thead> <thead>
<tr> <tr>
@ -20,7 +20,7 @@
<th class="text-center">{% trans 'IP' %}</th> <th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Username' %}</th> <th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Version' %}</th> <th class="text-center">{% trans 'Version' %}</th>
<th class="text-center">{% trans 'Connectivity'%}</th> {# <th class="text-center">{% trans 'Connectivity'%}</th>#}
<th class="text-center">{% trans 'Datetime' %}</th> <th class="text-center">{% trans 'Datetime' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
@ -33,62 +33,60 @@
{% include 'authentication/_mfa_confirm_modal.html' %} {% include 'authentication/_mfa_confirm_modal.html' %}
<script> <script>
var assetUserListUrl = "{% url "api-assets:asset-user-list" %}"; var defaultAssetUserListUrl = "{% url "api-assets:asset-user-list" %}";
var defaultAssetUserDetail = "{% url "api-assets:asset-user-detail" pk=DEFAULT_PK %}";
var assetUserTable; var assetUserTable;
var needPush = false; var defaultNeedPush = false;
var prefer = null;
var lastMFATime = "{{ request.session.MFA_VERIFY_TIME }}"; var lastMFATime = "{{ request.session.MFA_VERIFY_TIME }}";
var testDatetime = "{% trans 'Test datetime: ' %}"; var testDatetime = "{% trans 'Test datetime: ' %}";
var mfaVerifyTTL = "{{ SECURITY_MFA_VERIFY_TTL }}"; var mfaVerifyTTL = "{{ SECURITY_MFA_VERIFY_TTL }}";
var mfaNeedCheck = "{{ SECURITY_VIEW_AUTH_NEED_MFA }}" === "True"; var mfaNeedCheck = "{{ SECURITY_VIEW_AUTH_NEED_MFA }}" === "True";
var onlyLatestEl = "<span style='padding-right:20px'><input type='checkbox' id='only_latest'> {% trans 'Only latest version' %}</span>";
var onlyLatestChecked = false;
var systemUserId = "";
function initAssetUserTable() { function initAssetUserTable(option) {
if (!option) {
option = {}
}
var assetUserListUrl = option.assetUserListUrl || defaultAssetUserListUrl;
var needPush = option.needPush === undefined ? defaultNeedPush : option.needPush;
var options = { var options = {
ele: $('#asset_user_list_table'), ele: $('#asset_user_list_table'),
toggle: true, toggle: true,
columnDefs: [ columnDefs: [
{ {
targets: 5, createdCell: function (td, cellData) { targets: 5, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData.status == 1) {
innerHtml = '<i class="fa fa-circle text-navy"></i>'
} else if (cellData.status == 0) {
innerHtml = '<i class="fa fa-circle text-danger"></i>'
} else {
innerHtml = '<i class="fa fa-circle text-warning"></i>'
}
var dateManual = toSafeLocalDateStr(cellData.datetime);
var dataContent = testDatetime + dateManual;
innerHtml = "<a data-toggle='popover' data-content='" + dataContent + "'" + 'data-placement="auto bottom"' + ">" + innerHtml + "</a>";
$(td).html(innerHtml);
}
},
{
targets: 6, createdCell: function (td, cellData) {
var data = toSafeLocalDateStr(cellData); var data = toSafeLocalDateStr(cellData);
$(td).html(data); $(td).html(data);
}, },
}, },
{ {
targets: 7, createdCell: function (td, cellData, rowData) { targets: 6, createdCell: function (td, cellData, rowData) {
var view_btn = '<button class="btn btn-xs btn-primary m-l-xs btn-view-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans "View" %}</button>' var viewBtn = '<button class="btn btn-xs btn-primary m-l-xs btn-view-auth" DATA>{% trans "View" %}</button>';
var update_btn = '<li><a class="btn-update-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Update' %}</a></li>'; var updateBtn = '<li><a class="btn-update-auth" DATA>{% trans 'Update' %}</a></li>';
var test_btn = '<li><a class="btn-test-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Test' %}</a></li>'; var testBtn = '<li><a class="btn-test-auth" DATA>{% trans 'Test' %}</a></li>';
var push_btn = '<li><a class="btn-push-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Push' %}</a></li>'; var pushBtn = '<li><a class="btn-push-auth" DATA>{% trans 'Push' %}</a></li>';
if (needPush) { var delBtn = '<li><a class="btn-del-auth" DATA>{% trans 'Delete' %}</a></li>';
test_btn += push_btn; if (!needPush) {
pushBtn = ''
} }
var actions = '<div class="btn-group">' + view_btn +
var data = "data-hostname=hostname123 data-username=username123 data-uid=uid123 data-asset=asset123";
data = data.replaceAll("username123", rowData.username)
.replaceAll("hostname123", rowData.hostname)
.replaceAll("uid123", rowData.id)
.replaceAll("asset123", rowData.asset);
var actions = '<div class="btn-group">' + viewBtn +
' <button data-toggle="dropdown" class="btn btn-primary btn-xs dropdown-toggle">' + ' <button data-toggle="dropdown" class="btn btn-primary btn-xs dropdown-toggle">' +
' <span class="caret"></span>' + ' <span class="caret"></span>' +
' </button>' + ' </button>' +
' <ul class="dropdown-menu">' + ' <ul class="dropdown-menu">' +
update_btn + test_btn + updateBtn + delBtn + testBtn + pushBtn
' </ul>' + ' </ul>' +
' </div>'; ' </div>';
actions = actions.replaceAll("username123", rowData.username) actions = actions.replaceAll("DATA", data);
.replaceAll("hostname123", rowData.hostname)
.replaceAll("asset123", rowData.asset);
$(td).html(actions); $(td).html(actions);
}, },
width: '70px' width: '70px'
@ -98,21 +96,23 @@ function initAssetUserTable() {
columns: [ columns: [
{data: "id"}, {data: "hostname"}, {data: "ip"}, {data: "id"}, {data: "hostname"}, {data: "ip"},
{data: "username"}, {data: "version", orderable: false}, {data: "username"}, {data: "version", orderable: false},
{data: "connectivity"},
{data: "date_created", orderable: false}, {data: "date_created", orderable: false},
{data: "asset", orderable: false} {data: "asset", orderable: false}
], ],
op_html: $('#actions').html() op_html: $('#actions').html(),
lb_html: onlyLatestEl,
}; };
table = jumpserver.initServerSideDataTable(options); assetUserTable = jumpserver.initServerSideDataTable(options);
return table return assetUserTable
} }
$(document).ready(function(){ $(document).ready(function(){
}) })
.on('click', '.btn-view-auth', function () { .on('click', '.btn-view-auth', function () {
// 通知给view auth modal
authAssetId = $(this).data("asset"); authAssetId = $(this).data("asset");
authHostname = $(this).data("hostname"); authHostname = $(this).data("hostname");
authUsername = $(this).data('user'); authUsername = $(this).data('username');
authUid = $(this).data("uid");
if (!mfaNeedCheck){ if (!mfaNeedCheck){
$("#asset_user_auth_view").modal('show'); $("#asset_user_auth_view").modal('show');
return return
@ -133,29 +133,56 @@ $(document).ready(function(){
$("#asset_user_auth_view").modal("show"); $("#asset_user_auth_view").modal("show");
}) })
.on('click', '.btn-update-auth', function() { .on('click', '.btn-update-auth', function() {
authUsername = $(this).data("user") ; authUsername = $(this).data("username") ;
authHostname = $(this).data("hostname"); authHostname = $(this).data("hostname");
authAssetId = $(this).data("asset"); authAssetId = $(this).data("asset");
$("#asset_user_auth_update_modal").modal('show'); $("#asset_user_auth_update_modal").modal('show');
}) })
.on("click", '.btn-test-auth', function () { .on("click", '.btn-test-auth', function () {
authUsername = $(this).data("user") ; authUid = $(this).data('uid');
authAssetId = $(this).data("asset"); var theUrl = "{% url 'api-assets:asset-user-task-create'%}?id={{ DEFAULT_PK }}"
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id=" + authAssetId + "&username=" + authUsername; .replace("{{ DEFAULT_PK }}", authUid);
if (prefer) {
the_url = setUrlParam(the_url, "prefer", prefer)
}
var success = function (data) { var success = function (data) {
var task_id = data.task; var taskId = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(taskId);
}; };
requestApi({ requestApi({
url: the_url, url: theUrl,
method: 'GET', method: 'POST',
data: JSON.stringify({action: 'test'}),
success: success, success: success,
flash_message: false flash_message: false
}); });
}) })
.on('click', '.btn-del-auth', function () {
var uid = $(this).data("uid");
var theUrl = defaultAssetUserDetail.replace("{{ DEFAULT_PK }}", uid);
requestApi({
url: theUrl,
method: "DELETE",
success: function () {
assetUserTable.ajax.reload(null, false);
},
success_message: "{% trans 'Delete success' %}"
})
})
.on("change", '#only_latest', function () {
var checked = $("#only_latest").is(":checked");
if (checked === onlyLatestChecked) {
return
}
var ajaxUrl = assetUserTable.ajax.url();
if (checked) {
ajaxUrl = setUrlParam(ajaxUrl, 'latest', 1)
} else {
ajaxUrl = setUrlParam(ajaxUrl, 'latest', 0)
}
onlyLatestChecked = !onlyLatestChecked;
assetUserTable.ajax.url(ajaxUrl);
assetUserTable.ajax.reload();
})
</script> </script>

View File

@ -113,10 +113,11 @@ function initNodeTree(options) {
$.get(treeUrl, function (data, status) { $.get(treeUrl, function (data, status) {
zTree = $.fn.zTree.init($("#nodeTree"), setting, data); zTree = $.fn.zTree.init($("#nodeTree"), setting, data);
rootNodeAddDom(zTree, function () { rootNodeAddDom(zTree, function () {
const url = '{% url 'api-assets:refresh-nodes-cache' %}'; const url = '{% url 'api-assets:node-task-create' pk=DEFAULT_PK %}';
requestApi({ requestApi({
url: url, url: url,
method: 'GET', method: 'POST',
data: {action: "refresh_cache"},
flash_message: false, flash_message: false,
success: function () { success: function () {
initNodeTree(options); initNodeTree(options);
@ -173,20 +174,14 @@ function removeTreeNode() {
if (!current_node){ if (!current_node){
return return
} }
if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}");
} else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({ requestApi({
url: url, url: url,
method: "DELETE", method: "DELETE",
success: function () { success: function () {
zTree.removeNode(current_node); zTree.removeNode(current_node)
}
});
} }
})
} }
function editTreeNode() { function editTreeNode() {

View File

@ -34,6 +34,7 @@
{% bootstrap_field form.name layout="horizontal" %} {% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.login_mode layout="horizontal" %} {% bootstrap_field form.login_mode layout="horizontal" %}
{% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.username_same_with_user layout="horizontal" %}
{% bootstrap_field form.priority layout="horizontal" %} {% bootstrap_field form.priority layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %} {% bootstrap_field form.protocol layout="horizontal" %}
@ -63,6 +64,7 @@
{% bootstrap_field form.cmd_filters layout="horizontal" %} {% bootstrap_field form.cmd_filters layout="horizontal" %}
</div> </div>
<h3>{% trans 'Other' %}</h3> <h3>{% trans 'Other' %}</h3>
{% bootstrap_field form.sftp_root layout="horizontal" %}
{% bootstrap_field form.sudo layout="horizontal" %} {% bootstrap_field form.sudo layout="horizontal" %}
{% bootstrap_field form.shell layout="horizontal" %} {% bootstrap_field form.shell layout="horizontal" %}
{% bootstrap_field form.comment layout="horizontal" %} {% bootstrap_field form.comment layout="horizontal" %}
@ -226,6 +228,10 @@ $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
authFieldsDisplay(); authFieldsDisplay();
fieldDisplay(); fieldDisplay();
var checked = $("#id_username_same_with_user").prop('checked');
if (checked) {
$("#id_username").attr("disabled", true)
}
}) })
.on('change', auto_generate_key, function(){ .on('change', auto_generate_key, function(){
authFieldsDisplay(); authFieldsDisplay();
@ -246,7 +252,7 @@ $(document).ready(function () {
var data = form.serializeObject(); var data = form.serializeObject();
objectAttrsIsList(data, ['cmd_filters']); objectAttrsIsList(data, ['cmd_filters']);
objectAttrsIsBool(data, ["auto_generate_key", "auto_push"]); objectAttrsIsBool(data, ["auto_generate_key", "auto_push", "username_same_with_user"]);
data["private_key"] = $("#id_private_key").data('file'); data["private_key"] = $("#id_private_key").data('file');
var props = { var props = {
@ -261,6 +267,15 @@ $(document).ready(function () {
readFile($(this)).on("onload", function (evt, data) { readFile($(this)).on("onload", function (evt, data) {
$(this).data("file", data) $(this).data("file", data)
}) })
}).on("change", '#id_username_same_with_user', function () {
var checked = $(this).prop('checked');
var usernameRef = $("#id_username");
if (checked) {
usernameRef.val('');
usernameRef.attr("disabled", true)
} else {
usernameRef.attr("disabled", false)
}
}) })
</script> </script>

View File

@ -72,9 +72,9 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
assetUserListUrl = setUrlParam(assetUserListUrl, "admin_user_id", "{{ admin_user.id }}"); var assetUserListUrl = setUrlParam(defaultAssetUserListUrl, "prefer_id", "{{ admin_user.id }}");
prefer = "admin_user"; assetUserListUrl = setUrlParam(assetUserListUrl, "prefer", "admin_user");
initAssetUserTable(); initAssetUserTable({assetUserListUrl: assetUserListUrl});
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}"; var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";

View File

@ -61,7 +61,7 @@
</tr> </tr>
<tr> <tr>
<td>{% trans 'Created by' %}:</td> <td>{% trans 'Created by' %}:</td>
<td><b>{{ asset_group.created_by }}</b></td> <td><b>{{ admin_user.created_by }}</b></td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Comment' %}:</td> <td>{% trans 'Comment' %}:</td>

View File

@ -2,7 +2,7 @@
{% load i18n static %} {% load i18n static %}
{% block help_message %} {% block help_message %}
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%} {% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%} {% trans 'JumpServer users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
{% include '_csv_import_export.html' %} {% include '_csv_import_export.html' %}

View File

@ -73,19 +73,22 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
assetUserListUrl = setUrlParam(assetUserListUrl, "asset_id", "{{ asset.id }}"); assetUserListUrl = setUrlParam(defaultAssetUserListUrl, "asset_id", "{{ asset.id }}");
initAssetUserTable() initAssetUserTable({assetUserListUrl: assetUserListUrl})
}) })
.on('click', '#btn-bulk-test-connective', function () { .on('click', '#btn-bulk-test-connective', function () {
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}"; var assetId = "{{ asset.id }}";
var theUrl = "{% url 'api-assets:asset-user-task-create' %}?asset_id={{ DEFAULT_PK }}&latest=1"
.replace("{{ DEFAULT_PK }}", assetId);
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(task_id);
}; };
requestApi({ requestApi({
url: the_url, url: theUrl,
method: 'GET', method: 'POST',
data: {action: "test"},
success: success, success: success,
flash_message: false flash_message: false
}); });

View File

@ -268,16 +268,16 @@ function updateAssetNodes(nodes) {
} }
function refreshAssetHardware() { function refreshAssetHardware() {
var the_url = "{% url 'api-assets:asset-refresh' pk=asset.id %}"; var the_url = "{% url 'api-assets:asset-task-create' pk=asset.id %}";
var success = function(data) { var success = function(data) {
console.log(data);
var task_id = data.task; var task_id = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(task_id);
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
success: success, success: success,
method: 'GET' data: {action: "refresh"},
method: 'POST'
}); });
} }
@ -345,16 +345,15 @@ $(document).ready(function () {
}).on('click', '#btn_refresh_asset', function () { }).on('click', '#btn_refresh_asset', function () {
refreshAssetHardware() refreshAssetHardware()
}).on('click', '#btn-test-is-alive', function () { }).on('click', '#btn-test-is-alive', function () {
var the_url = "{% url 'api-assets:asset-alive-test' pk=asset.id %}"; var the_url = "{% url 'api-assets:asset-task-create' pk=asset.id %}";
var success = function(data) { var success = function(data) {
var task_id = data.task; var task_id = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(task_id);
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
method: 'GET', method: 'POST',
data: {action: "test"},
success: success success: success
}); });
}) })

View File

@ -258,6 +258,10 @@ $(document).ready(function(){
confirmButtonText: "{% trans 'Confirm' %}", confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false closeOnConfirm: false
},function () { },function () {
function fail() {
var msg = "{% trans 'Asset Deleting failed.' %}";
swal("{% trans 'Asset Delete' %}", msg, "error");
}
function success(data) { function success(data) {
url = setUrlParam(the_url, 'spm', data.spm); url = setUrlParam(the_url, 'spm', data.spm);
requestApi({ requestApi({
@ -268,13 +272,10 @@ $(document).ready(function(){
swal("{% trans 'Asset Delete' %}", msg, "success"); swal("{% trans 'Asset Delete' %}", msg, "success");
reloadTable(); reloadTable();
}, },
error: fail,
flash_message: false, flash_message: false,
}); });
} }
function fail() {
var msg = "{% trans 'Asset Deleting failed.' %}";
swal("{% trans 'Asset Delete' %}", msg, "error");
}
requestApi({ requestApi({
url: "{% url 'api-common:resources-cache' %}", url: "{% url 'api-common:resources-cache' %}",
method: 'POST', method: 'POST',
@ -306,6 +307,7 @@ $(document).ready(function(){
function doRemove() { function doRemove() {
if (!current_node_id) { if (!current_node_id) {
toastr.error("{% trans 'Please select node' %}");
return return
} }
@ -350,7 +352,7 @@ $(document).ready(function(){
}).on('click', '#menu_asset_move', function () { }).on('click', '#menu_asset_move', function () {
update_node_action = "move" update_node_action = "move"
}).on('click', '.btn-test-connective', function () { }).on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}"; var url = "{% url 'api-assets:node-task-create' pk=DEFAULT_PK %}";
if (!current_node_id) { if (!current_node_id) {
return null; return null;
} }
@ -362,12 +364,13 @@ $(document).ready(function(){
} }
requestApi({ requestApi({
url: the_url, url: the_url,
method: "GET", method: "POST",
data: {action: "test"},
success: success, success: success,
flash_message: false flash_message: false
}); });
}).on('click', '.btn-refresh-hardware', function () { }).on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}"; var url = "{% url 'api-assets:node-task-create' pk=DEFAULT_PK %}";
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id); var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
@ -376,7 +379,8 @@ $(document).ready(function(){
} }
requestApi({ requestApi({
url: the_url, url: the_url,
method: "GET", method: "POST",
data: {action: "refresh"},
success: success, success: success,
flash_message: false flash_message: false
}); });

View File

@ -63,10 +63,6 @@
<td>{% trans 'Date created' %}:</td> <td>{% trans 'Date created' %}:</td>
<td><b>{{ object.date_created }}</b></td> <td><b>{{ object.date_created }}</b></td>
</tr> </tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ object.created_by }}</b></td>
</tr>
<tr> <tr>
<td>{% trans 'Comment' %}:</td> <td>{% trans 'Comment' %}:</td>
<td><b>{{ object.comment }}</b></td> <td><b>{{ object.comment }}</b></td>

View File

@ -13,7 +13,12 @@
<a href="{% url 'assets:platform-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a> <a href="{% url 'assets:platform-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:platform-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a> <a class="btn btn-outline btn-default" {% if object.internal %} disabled="true" {% endif %} href="{% url 'assets:platform-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-del" {% if object.internal %} disabled="true" {% endif %}>
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -72,4 +77,15 @@
{% endblock %} {% endblock %}
{% block content_bottom_left %}{% endblock %} {% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script>
$(document).ready(function () {
}).on('click', '.btn-del', function () {
var $this = $(this);
var name = "{{ object.name}}";
var uid = "{{ object.id }}";
var the_url = '{% url "api-assets:asset-platform-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
var redirect_url = "{% url 'assets:platform-list' %}";
objectDelete($this, name, the_url, redirect_url);
})
</script>
{% endblock %} {% endblock %}

View File

@ -23,16 +23,23 @@
</li> </li>
<li class="active"> <li class="active">
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center"> <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %} <i class="fa fa-bar-chart-o"></i> {% trans 'Asset list' %}
</a> </a>
</li> </li>
{% if system_user.username_same_with_user %}
<li>
<a href="{% url 'assets:system-user-user' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'User list' %}
</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;"> <div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-title"> <div class="ibox-title">
<span style="float: left">{% trans 'Assets of ' %} <b>{{ system_user.name }} </b><span class="badge">{{ paginator.count }}</span></span> <span style="float: left"><b>{{ system_user.name }} </b><span class="badge">{{ paginator.count }}</span></span>
<div class="ibox-tools"> <div class="ibox-tools">
<a class="collapse-link"> <a class="collapse-link">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
@ -82,6 +89,30 @@
</table> </table>
</div> </div>
</div> </div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Assets' %}
</div>
<div class="panel-body">
<table class="table" id="add-assets">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Select assets' %}" id="id_assets" class="assets-select2 select2" style="width: 100%" multiple="" tabindex="4">
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-assets">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
</tbody>
</table>
</div>
</div>
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-heading"> <div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %} <i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
@ -92,7 +123,7 @@
<form> <form>
<tr> <tr>
<td colspan="2" class="no-borders"> <td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="nodes-select2" style="width: 100%" multiple="" tabindex="4"> <select data-placeholder="{% trans 'Select nodes' %}" id="node_selected" class="nodes-select2" style="width: 100%" multiple="" tabindex="4">
</select> </select>
</td> </td>
</tr> </tr>
@ -114,6 +145,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'assets/_asset_list_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
@ -133,7 +165,7 @@ function getRelationUrl(type) {
return theUrl; return theUrl;
} }
function addObjects(objectsId, type) { function addObjects(objectsId, type, success, fail) {
if (!objectsId || objectsId.length === 0) { if (!objectsId || objectsId.length === 0) {
return return
} }
@ -144,13 +176,21 @@ function addObjects(objectsId, type) {
data[type] = v; data[type] = v;
body.push(data) body.push(data)
}); });
requestApi({ if (!success) {
success = reloadPage
}
var option = {
url: theUrl, url: theUrl,
body: JSON.stringify(body), body: JSON.stringify(body),
method: "POST", method: "POST",
success: reloadPage success: success,
}); };
if (fail) {
option.error = fail;
} }
requestApi(option)
}
function removeObject(objectId, type, success) { function removeObject(objectId, type, success) {
if (!objectId) { if (!objectId) {
@ -197,9 +237,13 @@ function initNodeTable() {
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
nodesSelect2Init(".nodes-select2"); nodesSelect2Init(".nodes-select2");
assetUserListUrl = setUrlParam(assetUserListUrl, "system_user_id", "{{ system_user.id }}"); initAssetTreeModel('#id_assets');
needPush = true; var assetUserListUrl = setUrlParam(defaultAssetUserListUrl, "prefer_id", "{{ system_user.id }}");
initAssetUserTable(); assetUserListUrl = setUrlParam(assetUserListUrl, "prefer", "system_user");
initAssetUserTable({
assetUserListUrl: assetUserListUrl,
needPush: true
});
initNodeTable(); initNodeTable();
}) })
.on('click', '.btn-remove-from-node', function() { .on('click', '.btn-remove-from-node', function() {
@ -215,8 +259,31 @@ $(document).ready(function () {
var nodes = $("#node_selected").val(); var nodes = $("#node_selected").val();
addObjects(nodes, "node"); addObjects(nodes, "node");
}) })
.on('click', '#btn-add-to-assets', function () {
var assets = $("#id_assets").val();
var options = $("#id_assets").find(":selected");
var failed = function(s, data) {
var invalidIndex = [];
var validIndex = [];
$.each(data, function (k, v) {
if (isEmptyObject(v)) {
validIndex.push(k)
} else {
invalidIndex.push(k)
}
});
var invalidLabel = [];
$.each(invalidIndex, function (k, v) {
invalidLabel.push(options[v].text)
});
var errorMsg = "{% trans 'Have existed: ' %}";
errorMsg += invalidLabel.join(", ");
toastr.error(errorMsg)
};
addObjects(assets, "asset", null, failed);
})
.on('click', '.btn-push', function () { .on('click', '.btn-push', function () {
var theUrl = "{% url 'api-assets:system-user-push' pk=system_user.id %}"; var theUrl = "{% url 'api-assets:system-user-task-create' pk=system_user.id %}";
var error = function (data) { var error = function (data) {
alert(data) alert(data)
}; };
@ -224,18 +291,26 @@ $(document).ready(function () {
var taskId = data.task; var taskId = data.task;
showCeleryTaskLog(taskId); showCeleryTaskLog(taskId);
}; };
var data = {
action: 'push'
};
requestApi({ requestApi({
url: theUrl, url: theUrl,
error: error, error: error,
method: 'GET', data: data,
method: 'POST',
success: success success: success
}); });
}) })
.on('click', '.btn-push-auth', function () { .on('click', '.btn-push-auth', function () {
var $this = $(this); var $this = $(this);
var asset_id = $this.data('asset'); var assetId = $this.data('asset');
var theUrl = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}"; var username = $this.data("username");
theUrl = theUrl.replace("{{ DEFAULT_PK }}", asset_id); var theUrl = "{% url 'api-assets:system-user-task-create' pk=object.id %}?username=" + username;
var data = {
action: 'push',
asset: assetId,
};
var success = function (data) { var success = function (data) {
var taskId = data.task; var taskId = data.task;
showCeleryTaskLog(taskId); showCeleryTaskLog(taskId);
@ -245,13 +320,15 @@ $(document).ready(function () {
}; };
requestApi({ requestApi({
url: theUrl, url: theUrl,
method: 'GET', method: 'POST',
data: data,
success: success, success: success,
flash_message: false,
error: error error: error
}) })
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var theUrl = "{% url 'api-assets:system-user-connective' pk=system_user.id %}"; var theUrl = "{% url 'api-assets:system-user-task-create' pk=system_user.id %}";
var error = function (data) { var error = function (data) {
alert(data) alert(data)
}; };
@ -261,9 +338,11 @@ $(document).ready(function () {
}; };
requestApi({ requestApi({
url: theUrl, url: theUrl,
data: {action: "test"},
error: error, error: error,
method: 'GET', method: 'POST',
success: success success: success,
flash_message: false,
}); });
}) })
</script> </script>

View File

@ -15,7 +15,14 @@
{% if system_user.can_perm_to_asset %} {% if system_user.can_perm_to_asset %}
<li> <li>
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center"> <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %} <i class="fa fa-bar-chart-o"></i> {% trans 'Asset list' %}
</a>
</li>
{% endif %}
{% if system_user.username_same_with_user %}
<li>
<a href="{% url 'assets:system-user-user' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'User list' %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
@ -57,7 +64,11 @@
</tr> </tr>
<tr> <tr>
<td>{% trans 'Username' %}:</td> <td>{% trans 'Username' %}:</td>
{% if system_user.username_same_with_user %}
<td><b>{% trans 'Username same with user' %}</b></td>
{% else %}
<td><b>{{ system_user.username }}</b></td> <td><b>{{ system_user.username }}</b></td>
{% endif %}
</tr> </tr>
<tr> <tr>
<td>{% trans 'Login mode' %}:</td> <td>{% trans 'Login mode' %}:</td>
@ -95,7 +106,7 @@
</tr> </tr>
<tr> <tr>
<td>{% trans 'Created by' %}:</td> <td>{% trans 'Created by' %}:</td>
<td><b>{{ asset_group.created_by }}</b></td> <td><b>{{ system_user.created_by }}</b></td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Comment' %}:</td> <td>{% trans 'Comment' %}:</td>
@ -131,26 +142,6 @@
</span> </span>
</td> </td>
</tr> </tr>
{% if system_user.auto_push %}
<tr class="only-ssh-rdp">
<td width="50%">{% trans 'Push system user now' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
</span>
</td>
</tr>
{% endif %}
{% if system_user.is_need_test_asset_connective %}
<tr>
<td width="50%">{% trans 'Test assets connective' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -246,32 +237,7 @@ $(document).ready(function () {
var redirect_url = "{% url 'assets:system-user-list' %}"; var redirect_url = "{% url 'assets:system-user-list' %}";
objectDelete($this, name, the_url, redirect_url); objectDelete($this, name, the_url, redirect_url);
}) })
.on('click', '.btn-push', function () { .on('click', '#btn-binding-command-filters', function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var success = function (data) {
var task_id = data.task;
showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var success = function (data) {
var task_id = data.task;
showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
}).on('click', '#btn-binding-command-filters', function () {
var new_selected_cmd_filters = $.map($('#command_filters_selected').select2('data'), function (i) { var new_selected_cmd_filters = $.map($('#command_filters_selected').select2('data'), function (i) {
return i.id; return i.id;
}); });

View File

@ -2,9 +2,9 @@
{% load i18n %} {% load i18n %}
{% block help_message %} {% block help_message %}
{% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%} {% trans 'System user is JumpServer jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%}
{% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%} {% trans 'In simple terms, users log into JumpServer using their own username, and JumpServer uses system users to log into assets. '%}
{% trans 'When system users are created, if you choose auto push Jumpserver to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.' %} {% trans 'When system users are created, if you choose auto push JumpServer to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.' %}
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
@ -26,9 +26,6 @@
<th class="text-center">{% trans 'Protocol' %}</th> <th class="text-center">{% trans 'Protocol' %}</th>
<th class="text-center">{% trans 'Login mode' %}</th> <th class="text-center">{% trans 'Login mode' %}</th>
<th class="text-center">{% trans 'Asset' %}</th> <th class="text-center">{% trans 'Asset' %}</th>
{# <th class="text-center">{% trans 'Reachable' %}</th>#}
{# <th class="text-center">{% trans 'Unreachable' %}</th>#}
{# <th class="text-center">{% trans 'Ratio' %}</th>#}
<th class="text-center">{% trans 'Comment' %}</th> <th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
@ -40,6 +37,8 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var system_user_table = 0; var system_user_table = 0;
function initTable() { function initTable() {
var options = { var options = {
ele: $('#system_user_list_table'), ele: $('#system_user_list_table'),
@ -61,7 +60,7 @@ function initTable() {
ajax_url: '{% url "api-assets:system-user-list" %}', ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [ columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"},
{data: "login_mode"}, {data: "assets_amount", orderable: false }, {data: "login_mode"}, {data: "assets_amount", width: "60px"},
{data: "comment" }, {data: "id", orderable: false, width: "120px"} {data: "comment" }, {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()

View File

@ -0,0 +1,212 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<style>
.table.node_edit {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li>
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Asset list' %}
</a>
</li>
{% if system_user.username_same_with_user %}
<li class="active">
<a href="{% url 'assets:system-user-user' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'User list' %}
</a>
</li>
{% endif %}
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left"><b>{{ system_user.name }} </b><span class="badge">{{ paginator.count }}</span></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table table-striped table-bordered table-hover" id="user_list_table" style="width: 100%">
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all">
</th>
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Users' %}
</div>
<div class="panel-body">
<table class="table" id="add-users">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Select users' %}" id="id_users" class="users-select2 select2" style="width: 100%" multiple="" tabindex="4">
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-users">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var usersRelationUrl = "{% url 'api-assets:system-users-users-relation-list' %}?systemuser={{ system_user.id }}";
var userTable = null;
function initTable() {
var options = {
ele: $('#user_list_table'),
toggle: true,
ajax_url: usersRelationUrl,
columnDefs: [
{
targets: 2, createdCell: function (td, cellData, rowData) {
var removeBtn = '<button class="btn btn-xs btn-danger m-l-xs btn-remove" data-uid=UID>{% trans "Remove" %}</button>';
removeBtn = removeBtn.replace("UID", rowData.user);
$(td).html(removeBtn);
},
width: '70px'
}
],
columns: [
{data: "id"}, {data: "user_display"},
{data: "id", orderable: false}
],
op_html: $('#actions').html(),
};
userTable = jumpserver.initServerSideDataTable(options);
return userTable
}
function addUsers(objectsId, success, fail) {
if (!objectsId || objectsId.length === 0) {
return
}
var theUrl = usersRelationUrl;
var body = [];
objectsId.forEach(function (v) {
var data = {systemuser: "{{ object.id }}"};
data.user = v;
body.push(data)
});
if (!success) {
success = reloadPage
}
var option = {
url: theUrl,
body: JSON.stringify(body),
method: "POST",
success: success,
};
if (fail) {
option.error = fail;
}
requestApi(option)
}
function removeUser(userId, success) {
if (!userId) {
return
}
var theUrl = usersRelationUrl;
theUrl = setUrlParam(theUrl, 'user', userId);
if (!success) {
success = function () {
userTable.ajax.reload();
}
}
requestApi({
url: theUrl,
method: "DELETE",
success: success,
success_message: "{% trans "Remove success" %}"
});
}
$(document).ready(function () {
initTable();
usersSelect2Init('.users-select2');
})
.on('click', '.btn-remove', function() {
var userId = $(this).data("uid");
removeUser(userId);
})
.on('click', '#btn-add-users', function() {
var usersId = $('.users-select2').val();
var options = $(".users-select2").find(":selected");
var failed = function(s, data) {
var invalidIndex = [];
var validIndex = [];
$.each(data, function (k, v) {
if (isEmptyObject(v)) {
validIndex.push(k)
} else {
invalidIndex.push(k)
}
});
var invalidLabel = [];
$.each(invalidIndex, function (k, v) {
invalidLabel.push(options[v].text)
});
var errorMsg = "{% trans 'Have existed: ' %}";
errorMsg += invalidLabel.join(", ");
toastr.error(errorMsg)
};
addUsers(usersId, null, failed);
})
</script>
{% endblock %}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
#

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
#

View File

@ -21,82 +21,46 @@ router.register(r'domains', api.DomainViewSet, 'domain')
router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'gateways', api.GatewayViewSet, 'gateway')
router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter')
router.register(r'asset-users', api.AssetUserViewSet, 'asset-user') router.register(r'asset-users', api.AssetUserViewSet, 'asset-user')
router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info') router.register(r'asset-user-auth-infos', api.AssetUserAuthInfoViewSet, 'asset-user-auth-info')
router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user')
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
urlpatterns = [ urlpatterns = [
path('assets/<uuid:pk>/refresh/', path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'), path('assets/<uuid:pk>/platform/', api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'),
path('assets/<uuid:pk>/alive/', path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('assets/<uuid:pk>/platform/',
api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'),
path('asset-users/auth-info/', path('asset-users/tasks/', api.AssetUserTaskCreateAPI.as_view(), name='asset-user-task-create'),
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),
path('asset-users/test-connective/',
api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'),
path('admin-users/<uuid:pk>/nodes/', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-users/<uuid:pk>/auth/', api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-users/<uuid:pk>/connective/', api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-users/<uuid:pk>/assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('admin-users/<uuid:pk>/nodes/', path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), path('system-users/<uuid:pk>/assets/<uuid:aid>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('admin-users/<uuid:pk>/auth/', path('system-users/<uuid:pk>/tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'),
api.AdminUserAuthApi.as_view(), name='admin-user-auth'), path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
path('admin-users/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-users/<uuid:pk>/assets/',
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-users/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-users/<uuid:pk>/assets/<uuid:aid>/auth-info/',
api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-users/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-users/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-users/<uuid:pk>/assets/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-users/<uuid:pk>/assets/<uuid:aid>/test/',
api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
path('system-users/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('system-users/<uuid:pk>/cmd-filter-rules/',
api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'), path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'), path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
path('nodes/<uuid:pk>/children/', path('nodes/<uuid:pk>/children/', api.NodeChildrenApi.as_view(), name='node-children'),
api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'), path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
path('nodes/<uuid:pk>/children/add/', path('nodes/<uuid:pk>/children/add/', api.NodeAddChildrenApi.as_view(), name='node-add-children'),
api.NodeAddChildrenApi.as_view(), name='node-add-children'), path('nodes/<uuid:pk>/assets/', api.NodeAssetsApi.as_view(), name='node-assets'),
path('nodes/<uuid:pk>/assets/', path('nodes/<uuid:pk>/assets/add/', api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
api.NodeAssetsApi.as_view(), name='node-assets'), path('nodes/<uuid:pk>/assets/replace/', api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
path('nodes/<uuid:pk>/assets/add/', path('nodes/<uuid:pk>/assets/remove/', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
api.NodeAddAssetsApi.as_view(), name='node-add-assets'), path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
path('nodes/<uuid:pk>/assets/replace/',
api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
path('nodes/<uuid:pk>/assets/remove/',
api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
path('nodes/<uuid:pk>/refresh-hardware-info/',
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
path('nodes/<uuid:pk>/test-connective/',
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
path('nodes/cache/', api.RefreshNodesCacheApi.as_view(), name='refresh-nodes-cache'), path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
path('gateways/<uuid:pk>/test-connective/',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
] ]

View File

@ -39,6 +39,7 @@ urlpatterns = [
path('system-user/<uuid:pk>/update/', views.SystemUserUpdateView.as_view(), name='system-user-update'), path('system-user/<uuid:pk>/update/', views.SystemUserUpdateView.as_view(), name='system-user-update'),
path('system-user/<uuid:pk>/delete/', views.SystemUserDeleteView.as_view(), name='system-user-delete'), path('system-user/<uuid:pk>/delete/', views.SystemUserDeleteView.as_view(), name='system-user-delete'),
path('system-user/<uuid:pk>/asset/', views.SystemUserAssetView.as_view(), name='system-user-asset'), path('system-user/<uuid:pk>/asset/', views.SystemUserAssetView.as_view(), name='system-user-asset'),
path('system-user/<uuid:pk>/user/', views.SystemUserUserView.as_view(), name='system-user-user'),
path('label/', views.LabelListView.as_view(), name='label-list'), path('label/', views.LabelListView.as_view(), name='label-list'),
path('label/create/', views.LabelCreateView.as_view(), name='label-create'), path('label/create/', views.LabelCreateView.as_view(), name='label-create'),

View File

@ -5,43 +5,35 @@ from treelib.exceptions import NodeIDAbsentError
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from common.utils import get_object_or_none, get_logger, timeit from common.utils import get_logger, timeit, lazyproperty
from .models import SystemUser, Asset from .models import Asset, Node
logger = get_logger(__file__) logger = get_logger(__file__)
def get_system_user_by_name(name):
system_user = get_object_or_none(SystemUser, name=name)
return system_user
def get_system_user_by_id(id):
system_user = get_object_or_none(SystemUser, id=id)
return system_user
class TreeService(Tree): class TreeService(Tree):
tag_sep = ' / ' tag_sep = ' / '
def __init__(self, *args, **kwargs): @staticmethod
super().__init__(*args, **kwargs) @timeit
self.nodes_assets_map = defaultdict(set) def get_nodes_assets_map():
self.all_nodes_assets_map = {} nodes_assets_map = defaultdict(set)
self._invalid_assets = frozenset() asset_node_list = Node.assets.through.objects.values_list(
'asset', 'node__key'
)
for asset_id, key in asset_node_list:
nodes_assets_map[key].add(asset_id)
return nodes_assets_map
@classmethod @classmethod
@timeit @timeit
def new(cls): def new(cls):
from .models import Node from .models import Node
from orgs.utils import tmp_to_root_org
with tmp_to_root_org():
all_nodes = list(Node.objects.all().values("key", "value")) all_nodes = list(Node.objects.all().values("key", "value"))
all_nodes.sort(key=lambda x: len(x["key"].split(":"))) all_nodes.sort(key=lambda x: len(x["key"].split(":")))
tree = cls() tree = cls()
tree.create_node(tag='', identifier='') tree.create_node(tag='', identifier='', data={})
for node in all_nodes: for node in all_nodes:
key = node["key"] key = node["key"]
value = node["value"] value = node["value"]
@ -53,20 +45,13 @@ class TreeService(Tree):
tree.init_assets() tree.init_assets()
return tree return tree
@timeit
def init_assets(self): def init_assets(self):
from orgs.utils import tmp_to_root_org node_assets_map = self.get_nodes_assets_map()
self.all_nodes_assets_map = {} for node in self.all_nodes_itr():
self.nodes_assets_map = defaultdict(set) key = node.identifier
with tmp_to_root_org(): assets = node_assets_map.get(key, set())
queryset = Asset.objects.all().values_list('id', 'nodes__key') data = {"assets": assets, "all_assets": None}
invalid_assets = Asset.objects.filter(is_active=False)\ node.data = data
.values_list('id', flat=True)
self._invalid_assets = frozenset(invalid_assets)
for asset_id, key in queryset:
if not key:
continue
self.nodes_assets_map[key].add(asset_id)
def safe_create_node(self, **kwargs): def safe_create_node(self, **kwargs):
parent = kwargs.get("parent") parent = kwargs.get("parent")
@ -125,32 +110,43 @@ class TreeService(Tree):
parent = self.copy_node(parent) parent = self.copy_node(parent)
return parent return parent
@lazyproperty
def invalid_assets(self):
assets = Asset.objects.filter(is_active=False).values_list('id', flat=True)
return assets
def set_assets(self, nid, assets): def set_assets(self, nid, assets):
self.nodes_assets_map[nid] = set(assets) node = self.get_node(nid)
if node.data is None:
node.data = {}
node.data["assets"] = assets
def assets(self, nid): def assets(self, nid):
assets = self.nodes_assets_map[nid] node = self.get_node(nid)
return assets return node.data.get("assets", set())
def valid_assets(self, nid): def valid_assets(self, nid):
return set(self.assets(nid)) - set(self._invalid_assets) return set(self.assets(nid)) - set(self.invalid_assets)
def all_assets(self, nid): def all_assets(self, nid):
assets = self.all_nodes_assets_map.get(nid) node = self.get_node(nid)
if assets: if node.data is None:
return assets node.data = {}
assets = set(self.assets(nid)) all_assets = node.data.get("all_assets")
if all_assets is not None:
return all_assets
all_assets = set(self.assets(nid))
try: try:
children = self.children(nid) children = self.children(nid)
except NodeIDAbsentError: except NodeIDAbsentError:
children = [] children = []
for child in children: for child in children:
assets.update(self.all_assets(child.identifier)) all_assets.update(self.all_assets(child.identifier))
self.all_nodes_assets_map[nid] = assets node.data["all_assets"] = all_assets
return assets return all_assets
def all_valid_assets(self, nid): def all_valid_assets(self, nid):
return set(self.all_assets(nid)) - set(self._invalid_assets) return set(self.all_assets(nid)) - set(self.invalid_assets)
def assets_amount(self, nid): def assets_amount(self, nid):
return len(self.all_assets(nid)) return len(self.all_assets(nid))
@ -186,15 +182,12 @@ class TreeService(Tree):
else: else:
# logger.debug('Add node: {}'.format(node.identifier)) # logger.debug('Add node: {}'.format(node.identifier))
self.add_node(node, parent) self.add_node(node, parent)
# #
# def __getstate__(self): # def __getstate__(self):
# self.mutex = None # self.mutex = None
# self.all_nodes_assets_map = {}
# self.nodes_assets_map = {}
# return self.__dict__ # return self.__dict__
#
def __setstate__(self, state): # def __setstate__(self, state):
self.__dict__ = state # self.__dict__ = state
if '_invalid_assets' not in state:
self._invalid_assets = frozenset()
# self.mutex = threading.Lock()

View File

@ -106,7 +106,7 @@ class AdminUserAssetsView(PermissionsMixin, SingleObjectMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Assets'), 'app': _('Assets'),
'action': _('Admin user detail'), 'action': _('Admin user assets'),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -48,6 +48,9 @@ class PlatformUpdateView(generic.UpdateView):
model = Platform model = Platform
template_name = 'assets/platform_create_update.html' template_name = 'assets/platform_create_update.html'
def post(self, *args, **kwargs):
pass
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
meta_form = PlatformMetaForm(initial=self.object.meta) meta_form = PlatformMetaForm(initial=self.object.meta)

View File

@ -17,6 +17,7 @@ __all__ = [
'SystemUserCreateView', 'SystemUserUpdateView', 'SystemUserCreateView', 'SystemUserUpdateView',
'SystemUserDetailView', 'SystemUserDeleteView', 'SystemUserDetailView', 'SystemUserDeleteView',
'SystemUserAssetView', 'SystemUserListView', 'SystemUserAssetView', 'SystemUserListView',
'SystemUserUserView',
] ]
@ -100,7 +101,22 @@ class SystemUserAssetView(PermissionsMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('assets'), 'app': _('assets'),
'action': _('System user asset'), 'action': _('System user assets'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class SystemUserUserView(PermissionsMixin, DetailView):
model = SystemUser
template_name = 'assets/system_user_users.html'
context_object_name = 'system_user'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _('assets'),
'action': _('System user users'),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from .backends import *
from .callback import *

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
from django_cas_ng.backends import CASBackend as _CASBackend
__all__ = ['CASBackend']
class CASBackend(_CASBackend):
def user_can_authenticate(self, user):
return True

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
#
from django.contrib.auth import get_user_model
User = get_user_model()
def cas_callback(response):
username = response['username']
user, user_created = User.objects.get_or_create(username=username)
profile, created = user.get_profile()
profile.role = response['attributes']['role']
profile.birth_date = response['attributes']['birth_date']
profile.save()

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
import django_cas_ng.views
urlpatterns = [
path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
]

View File

@ -29,26 +29,27 @@ class LDAPAuthorizationBackend(LDAPBackend):
def pre_check(self, username, password): def pre_check(self, username, password):
if not settings.AUTH_LDAP: if not settings.AUTH_LDAP:
return False error = 'Not enabled auth ldap'
logger.info('Authentication LDAP backend') return False, error
if not username: if not username:
logger.info('Authenticate failed: username is None') error = 'Username is None'
return False return False, error
if not password: if not password:
logger.info('Authenticate failed: password is None') error = 'Password is None'
return False return False, error
if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS: if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS:
user_model = self.get_user_model() user_model = self.get_user_model()
exist = user_model.objects.filter(username=username).exists() exist = user_model.objects.filter(username=username).exists()
if not exist: if not exist:
msg = 'Authentication failed: user ({}) is not in the user list' error = 'user ({}) is not in the user list'.format(username)
logger.info(msg.format(username)) return False, error
return False return True, ''
return True
def authenticate(self, request=None, username=None, password=None, **kwargs): def authenticate(self, request=None, username=None, password=None, **kwargs):
match = self.pre_check(username, password) logger.info('Authentication LDAP backend')
match, msg = self.pre_check(username, password)
if not match: if not match:
logger.info('Authenticate failed: {}'.format(msg))
return None return None
ldap_user = LDAPUser(self, username=username.strip(), request=request) ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password) user = self.authenticate_ldap_user(ldap_user, password)
@ -130,5 +131,5 @@ class LDAPUser(_LDAPUser):
setattr(self._user, field, value) setattr(self._user, field, value)
email = getattr(self._user, 'email', '') email = getattr(self._user, 'email', '')
email = construct_user_email(email, self._user.username) email = construct_user_email(self._user.username, email)
setattr(self._user, 'email', email) setattr(self._user, 'email', email)

View File

@ -11,6 +11,7 @@ from users.utils import (
reason_password_failed = 'password_failed' reason_password_failed = 'password_failed'
reason_mfa_failed = 'mfa_failed' reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist' reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired' reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid' reason_user_invalid = 'user_invalid'
@ -18,7 +19,8 @@ reason_user_inactive = 'user_inactive'
reason_choices = { reason_choices = {
reason_password_failed: _('Username/password check failed'), reason_password_failed: _('Username/password check failed'),
reason_mfa_failed: _('MFA authentication failed'), reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'),
reason_user_not_exist: _("Username does not exist"), reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"), reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'), reason_user_invalid: _('Disabled or expired'),
@ -46,6 +48,7 @@ block_login_msg = _(
mfa_failed_msg = _("MFA code invalid, or ntp sync server time") mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
mfa_required_msg = _("MFA required") mfa_required_msg = _("MFA required")
mfa_unset_msg = _("MFA not set, please set it first")
login_confirm_required_msg = _("Login confirm required") login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept") login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}") login_confirm_error_msg = _("Login confirm ticket was {}")
@ -116,6 +119,16 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
super().__init__(username=username, request=request) super().__init__(username=username, request=request)
class MFAUnsetError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_unset
msg = mfa_unset_msg
def __init__(self, user, request, url):
super().__init__(username=user.username, request=request)
self.user = user
self.url = url
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError): class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login' error = 'block_login'

View File

@ -6,7 +6,7 @@ from django.conf import settings
from common.utils import get_object_or_none, get_request_ip, get_logger from common.utils import get_object_or_none, get_request_ip, get_logger
from users.models import User from users.models import User
from users.utils import ( from users.utils import (
is_block_login, clean_failed_count, increase_login_failed_count, is_block_login, clean_failed_count
) )
from . import errors from . import errors
from .utils import check_user_valid from .utils import check_user_valid
@ -91,8 +91,9 @@ class AuthMixin:
return return
if not user.mfa_enabled: if not user.mfa_enabled:
return return
if not user.otp_secret_key and user.mfa_is_otp(): unset, url = user.mfa_enabled_but_not_set()
return if unset:
raise errors.MFAUnsetError(user, self.request, url)
raise errors.MFARequiredError() raise errors.MFARequiredError()
def check_user_mfa(self, code): def check_user_mfa(self, code):

View File

@ -14,7 +14,7 @@
<label for="mfa" class="col-sm-2 control-label">{% trans 'MFA' %}</label> <label for="mfa" class="col-sm-2 control-label">{% trans 'MFA' %}</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" id="mfa" class="form-control input-sm" name="mfa"> <input type="text" id="mfa" class="form-control input-sm" name="mfa">
<span id="mfa_error" class="help-block">{% trans "Need otp auth for view auth" %}</span> <span id="mfa_error" class="help-block">{% trans "Need MFA for view auth" %}</span>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
<a class="btn btn-primary btn-sm btn-mfa">{% trans "Confirm" %}</a> <a class="btn btn-primary btn-sm btn-mfa">{% trans "Confirm" %}</a>

View File

@ -1,49 +1,12 @@
{% extends '_base_only_msg_content.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<!DOCTYPE html>
<html>
<head> {% block content_title %}
<meta charset="utf-8"> {% trans 'Login' %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock %}
<title>Jumpserver</title>
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style>
.captcha {
float: right;
}
</style>
</head>
<body class="gray-bg"> {% block content %}
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold" style="text-align: center">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
{% 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." %}
</p>
<p>
{% 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." %}
</p>
<p>
{% 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.' %}
</p>
<p>
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>
<div class="col-md-6">
<div class="ibox-content">
<div>
<img src="{{ LOGO_URL }}" width="60" height="60">
<span class="font-bold text-center" style="font-size: 24px; font-family: inherit; margin-left: 20px">{% trans 'Login' %}</span>
</div>
<form class="m-t" role="form" method="post" action=""> <form class="m-t" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %} {% if form.non_field_errors %}
@ -101,16 +64,4 @@
{% endif %} {% endif %}
</form> </form>
{% endblock %}
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
{% include '_copyright.html' %}
</div>
</div>
</div>
</body>
</html>

View File

@ -1,88 +1,32 @@
{% extends '_base_only_content.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<!DOCTYPE html>
<html>
<head> {% block title %}
<meta charset="utf-8"> {% trans 'MFA' %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock %}
<title> {{ JMS_TITLE }} </title>
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
<style>
.captcha {
float: right;
}
</style>
</head>
<body class="gray-bg">
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
{% 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." %}
</p>
<p>
{% 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." %}
</p>
<p>
{% 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.' %}
</p>
<p>
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>
<div class="col-md-6">
<div class="ibox-content">
<div>
<img src="{% static 'img/logo.png' %}" width="60" height="60">
<span class="font-bold text-center" style="font-size: 24px; font-family: inherit; margin-left: 20px">{% trans 'MFA certification' %}</span>
</div>
<div class="m-t">
<div class="form-group">
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
<div class="text-center">
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
</div>
<p style="margin: 30px auto">&nbsp;{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
</div>
{% block content %}
<form class="m-t" role="form" method="post" action=""> <form class="m-t" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
{% if 'otp_code' in form.errors %} {% if 'otp_code' in form.errors %}
<p class="red-fonts">{{ form.otp_code.errors.as_text }}</p> <p class="red-fonts">{{ form.otp_code.errors.as_text }}</p>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="otp_code" placeholder="{% trans 'Six figures' %}" required=""> <select class="form-control">
<option value="otp" selected>{% trans 'One-time password' %}</option>
</select>
</div>
<div class="form-group">
<input type="text" class="form-control" name="otp_code" placeholder="" required="" autofocus="autofocus">
<span class="help-block">
{% trans 'Open Google Authenticator and enter the 6-bit dynamic code' %}
</span>
</div> </div>
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button> <button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
<a href="#"> <div>
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small> <small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
</a> </div>
</form> </form>
</div> {% endblock %}
<p class="m-t">
</p>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
{% include '_copyright.html' %}
</div>
</div>
</div>
</body>
</html>

View File

@ -1,8 +1,6 @@
# coding:utf-8 # coding:utf-8
# #
from __future__ import absolute_import
from django.urls import path, include from django.urls import path, include
from .. import views from .. import views
@ -10,13 +8,14 @@ from .. import views
app_name = 'authentication' app_name = 'authentication'
urlpatterns = [ urlpatterns = [
# openid
path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')),
# login # login
path('login/', views.UserLoginView.as_view(), name='login'), path('login/', views.UserLoginView.as_view(), name='login'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'), path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'), path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
path('logout/', views.UserLogoutView.as_view(), name='logout'), path('logout/', views.UserLogoutView.as_view(), name='logout'),
# openid
path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')),
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
] ]

View File

@ -20,7 +20,7 @@ from django.urls import reverse_lazy
from common.utils import get_request_ip, get_object_or_none from common.utils import get_request_ip, get_object_or_none
from users.utils import ( from users.utils import (
redirect_user_first_login_or_index, set_tmp_user_to_cache redirect_user_first_login_or_index
) )
from .. import forms, mixins, errors from .. import forms, mixins, errors
@ -52,17 +52,29 @@ class UserLoginView(mixins.AuthMixin, FormView):
template_name = 'authentication/xpack_login.html' template_name = 'authentication/xpack_login.html'
return template_name return template_name
def get_redirect_url_if_need(self, request):
redirect_url = ''
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1
if self.request.GET.get("admin", 0):
return None
if settings.AUTH_OPENID:
redirect_url = reverse("authentication:openid:openid-login")
elif settings.AUTH_CAS:
redirect_url = reverse(settings.CAS_LOGIN_URL_NAME)
if redirect_url:
query_string = request.GET.urlencode()
redirect_url = "{}?{}".format(redirect_url, query_string)
return redirect_url
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.user.is_staff: if request.user.is_staff:
return redirect(redirect_user_first_login_or_index( return redirect(redirect_user_first_login_or_index(
request, self.redirect_field_name) request, self.redirect_field_name)
) )
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 redirect_url = self.get_redirect_url_if_need(request)
if settings.AUTH_OPENID and not self.request.GET.get('admin', 0): if redirect_url:
query_string = request.GET.urlencode() return redirect(redirect_url)
openid_login_url = reverse_lazy("authentication:openid:openid-login")
login_url = "{}?{}".format(openid_login_url, query_string)
return redirect(login_url)
request.session.set_test_cookie() request.session.set_test_cookie()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -127,12 +139,9 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
return self.format_redirect_url(self.login_otp_url) return self.format_redirect_url(self.login_otp_url)
except errors.LoginConfirmBaseError: except errors.LoginConfirmBaseError:
return self.format_redirect_url(self.login_confirm_url) return self.format_redirect_url(self.login_confirm_url)
except errors.MFAUnsetError as e:
return e.url
else: else:
# 启用但是没有设置otp, 排除radius
if user.mfa_enabled_but_not_set():
# 1,2,mfa_setting & F
set_tmp_user_to_cache(self.request, user)
return reverse('users:user-otp-enable-authentication')
auth_login(self.request, user) auth_login(self.request, user)
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)
self.clear_auth_mark() self.clear_auth_mark()
@ -174,8 +183,17 @@ class UserLoginWaitConfirmView(TemplateView):
class UserLogoutView(TemplateView): class UserLogoutView(TemplateView):
template_name = 'flash_message_standalone.html' template_name = 'flash_message_standalone.html'
@staticmethod
def get_backend_logout_url():
# if settings.AUTH_CAS:
# return settings.CAS_LOGOUT_URL_NAME
return None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
auth_logout(request) auth_logout(request)
backend_logout_url = self.get_backend_logout_url()
if backend_logout_url:
return redirect(backend_logout_url)
next_uri = request.COOKIES.get("next") next_uri = request.COOKIES.get("next")
if next_uri: if next_uri:
return redirect(next_uri) return redirect(next_uri)

View File

@ -16,7 +16,7 @@ provide this ability, not common, You should write it on your app utils.
## Celery usage ## Celery usage
Jumpserver use celery to run task async. Using redis as the broker, so JumpServer use celery to run task async. Using redis as the broker, so
you should run a redis instance you should run a redis instance
#### Run redis #### Run redis

View File

@ -1,13 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from hashlib import md5
from threading import Thread
from django.core.cache import cache
from django.http import JsonResponse from django.http import JsonResponse
from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from common.drf.filters import IDSpmFilter, CustomFilter from common.drf.filters import IDSpmFilter, CustomFilter
from ..utils import lazyproperty
__all__ = [ __all__ = [
"JSONResponseMixin", "CommonApiMixin", "JSONResponseMixin", "CommonApiMixin",
"IDSpmFilterMixin", "IDSpmFilterMixin", 'AsyncApiMixin',
] ]
@ -62,3 +69,122 @@ class ExtraFilterFieldsMixin:
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin): class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin):
pass pass
class InterceptMixin:
def dispatch(self, request, *args, **kwargs):
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = self.do(handler, request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
class AsyncApiMixin(InterceptMixin):
def get_request_user_id(self):
user = self.request.user
if hasattr(user, 'id'):
return str(user.id)
return ''
@lazyproperty
def async_cache_key(self):
method = self.request.method
path = self.get_request_md5()
user = self.get_request_user_id()
key = '{}_{}_{}'.format(method, path, user)
return key
def get_request_md5(self):
path = self.request.path
query = {k: v for k, v in self.request.GET.items()}
query.pop("_", None)
query.pop('refresh', None)
query = "&".join(["{}={}".format(k, v) for k, v in query.items()])
full_path = "{}?{}".format(path, query)
return md5(full_path.encode()).hexdigest()
@lazyproperty
def initial_data(self):
data = {
"status": "running",
"start_time": time.time(),
"key": self.async_cache_key,
}
return data
def get_cache_data(self):
key = self.async_cache_key
if self.is_need_refresh():
cache.delete(key)
return None
data = cache.get(key)
return data
def do(self, handler, *args, **kwargs):
if not self.is_need_async():
return handler(*args, **kwargs)
resp = self.do_async(handler, *args, **kwargs)
return resp
def is_need_refresh(self):
if self.request.GET.get("refresh"):
return True
return False
def is_need_async(self):
return False
def do_async(self, handler, *args, **kwargs):
data = self.get_cache_data()
if not data:
t = Thread(
target=self.do_in_thread,
args=(handler, *args),
kwargs=kwargs
)
t.start()
resp = Response(self.initial_data)
return resp
status = data.get("status")
resp = data.get("resp")
if status == "ok" and resp:
resp = Response(**resp)
else:
resp = Response(data)
return resp
def do_in_thread(self, handler, *args, **kwargs):
key = self.async_cache_key
data = self.initial_data
cache.set(key, data, 600)
try:
response = handler(*args, **kwargs)
data["status"] = "ok"
data["resp"] = {
"data": response.data,
"status": response.status_code
}
cache.set(key, data, 600)
except Exception as e:
data["error"] = str(e)
data["status"] = "error"
cache.set(key, data, 600)

View File

@ -5,7 +5,6 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
__all__ = [ __all__ = [
"NoDeleteManager", "NoDeleteModelMixin", "NoDeleteQuerySet", "NoDeleteManager", "NoDeleteModelMixin", "NoDeleteQuerySet",
"CommonModelMixin" "CommonModelMixin"
@ -65,3 +64,5 @@ class DebugQueryManager(models.Manager):
print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
queryset = super().get_queryset() queryset = super().get_queryset()
return queryset return queryset

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from itertools import chain
from .utils import lazyproperty
class Stack(list): class Stack(list):
@ -23,3 +25,82 @@ class Stack(list):
def push(self, item): def push(self, item):
self.append(item) self.append(item)
class QuerySetChain:
def __init__(self, querysets):
self.querysets = querysets
@lazyproperty
def querysets_counts(self):
counts = [s.count() for s in self.querysets]
return counts
def count(self):
return self.total_count
@lazyproperty
def total_count(self):
return sum(self.querysets_counts)
def __iter__(self):
self._chain = chain(*self.querysets)
return self
def __next__(self):
return next(self._chain)
def __getitem__(self, ndx):
querysets_count_zip = zip(self.querysets, self.querysets_counts)
length = 0 # 加上本数组后的大数组长度
pre_length = 0 # 不包含本数组的大数组长度
items = [] # 返回的值
loop = 0
if isinstance(ndx, slice):
ndx_start = ndx.start or 0
ndx_stop = ndx.stop or self.total_count
ndx_step = ndx.step or 1
else:
ndx_start = ndx
ndx_stop, ndx_step = None, None
for queryset, count in querysets_count_zip:
length += count
loop += 1
# 取当前数组的start角标, 存在3中情况
# 1. start角标在当前数组
if length > ndx_start >= pre_length:
start = ndx_start - pre_length
# print("[loop {}] Start is: {}".format(loop, start))
if ndx_step is None:
return queryset[start]
# 2. 不包含当前数组,因为起始已经超过了当前数组的长度
elif ndx_start >= length:
pre_length += count
continue
# 3. 不在当前数组但是应该从当前数组0开始计算
else:
start = 0
# 可能取单个值, ndx_stop 为None, 不应该再找
if ndx_stop is None:
pre_length += count
continue
# 取当前数组的stop角标, 存在2中情况
# 不存在第3中情况是因为找到了会提交结束循环
# 1. 结束角标小于length代表 结束位在当前数组上
if ndx_stop < length:
stop = ndx_stop - pre_length
# 2. 结束位置包含改数组到了最后
else:
stop = count
# print("[loop {}] Slice: {} {} {}".format(loop, start, stop, ndx_step))
items.extend(list(queryset[slice(start, stop, ndx_step)]))
pre_length += count
# 如果结束再当前数组,则结束循环
if ndx_stop < length:
break
return items

View File

@ -199,11 +199,15 @@ logger = get_logger(__name__)
def timeit(func): def timeit(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
logger.debug("Start call: {}".format(func.__name__)) if hasattr(func, '__name__'):
name = func.__name__
else:
name = func
logger.debug("Start call: {}".format(name))
now = time.time() now = time.time()
result = func(*args, **kwargs) result = func(*args, **kwargs)
using = (time.time() - now) * 1000 using = (time.time() - now) * 1000
msg = "End call {}, using: {:.1f}ms".format(func.__name__, using) msg = "End call {}, using: {:.1f}ms".format(name, using)
logger.debug(msg) logger.debug(msg)
return result return result
return wrapper return wrapper

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
#
import socket
import struct
import random
def random_datetime(date_start, date_end):
random_delta = (date_end - date_start) * random.random()
return date_start + random_delta
def random_ip():
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
# def strTimeProp(start, end, prop, fmt):
# time_start = time.mktime(time.strptime(start, fmt))
# time_end = time.mktime(time.strptime(end, fmt))
# ptime = time_start + prop * (time_end - time_start)
# return int(ptime)
#
#
# def randomTimestamp(start, end, fmt='%Y-%m-%d %H:%M:%S'):
# return strTimeProp(start, end, random.random(), fmt)
#
#
# def randomDate(start, end, frmt='%Y-%m-%d %H:%M:%S'):
# return time.strftime(frmt, time.localtime(strTimeProp(start, end, random.random(), frmt)))
#
#
# def randomTimestampList(start, end, n, frmt='%Y-%m-%d %H:%M:%S'):
# return [randomTimestamp(start, end, frmt) for _ in range(n)]
#
#
# def randomDateList(start, end, n, frmt='%Y-%m-%d %H:%M:%S'):
# return [randomDate(start, end, frmt) for _ in range(n)]

View File

@ -84,11 +84,10 @@ class Config(dict):
:param defaults: an optional dictionary of default values :param defaults: an optional dictionary of default values
""" """
defaults = { defaults = {
# Django Config # Django Config, Must set before start
'SECRET_KEY': '', 'SECRET_KEY': '',
'BOOTSTRAP_TOKEN': '', 'BOOTSTRAP_TOKEN': '',
'DEBUG': True, 'DEBUG': True,
'SITE_URL': 'http://localhost:8080',
'LOG_LEVEL': 'DEBUG', 'LOG_LEVEL': 'DEBUG',
'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'),
'DB_ENGINE': 'mysql', 'DB_ENGINE': 'mysql',
@ -100,10 +99,13 @@ class Config(dict):
'REDIS_HOST': '127.0.0.1', 'REDIS_HOST': '127.0.0.1',
'REDIS_PORT': 6379, 'REDIS_PORT': 6379,
'REDIS_PASSWORD': '', 'REDIS_PASSWORD': '',
# Default value
'REDIS_DB_CELERY': 3, 'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4, 'REDIS_DB_CACHE': 4,
'REDIS_DB_SESSION': 5, 'REDIS_DB_SESSION': 5,
'REDIS_DB_WS': 6, 'REDIS_DB_WS': 6,
'SITE_URL': 'http://localhost:8080',
'CAPTCHA_TEST_MODE': None, 'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600 * 24, 'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25, 'DISPLAY_PER_PAGE': 25,
@ -140,6 +142,7 @@ class Config(dict):
'AUTH_OPENID_CLIENT_SECRET': '', 'AUTH_OPENID_CLIENT_SECRET': '',
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
'AUTH_OPENID_SHARE_SESSION': True, 'AUTH_OPENID_SHARE_SESSION': True,
'CAS_ROOT_PROXIED_AS': '',
'AUTH_RADIUS': False, 'AUTH_RADIUS': False,
'RADIUS_SERVER': 'localhost', 'RADIUS_SERVER': 'localhost',
@ -148,8 +151,13 @@ class Config(dict):
'RADIUS_ENCRYPT_PASSWORD': True, 'RADIUS_ENCRYPT_PASSWORD': True,
'OTP_IN_RADIUS': False, 'OTP_IN_RADIUS': False,
'AUTH_CAS': False,
'CAS_SERVER_URL': "http://host/cas/",
'CAS_LOGOUT_COMPLETELY': True,
'CAS_VERSION': 3,
'OTP_VALID_WINDOW': 2, 'OTP_VALID_WINDOW': 2,
'OTP_ISSUER_NAME': 'Jumpserver', 'OTP_ISSUER_NAME': 'JumpServer',
'EMAIL_SUFFIX': 'jumpserver.org', 'EMAIL_SUFFIX': 'jumpserver.org',
'TERMINAL_PASSWORD_AUTH': True, 'TERMINAL_PASSWORD_AUTH': True,
@ -179,6 +187,7 @@ class Config(dict):
'HTTP_LISTEN_PORT': 8080, 'HTTP_LISTEN_PORT': 8080,
'WS_LISTEN_PORT': 8070, 'WS_LISTEN_PORT': 8070,
'LOGIN_LOG_KEEP_DAYS': 90, 'LOGIN_LOG_KEEP_DAYS': 90,
'TASK_LOG_KEEP_DAYS': 10,
'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'ASSETS_PERM_CACHE_TIME': 3600 * 24,
'SECURITY_MFA_VERIFY_TTL': 3600, 'SECURITY_MFA_VERIFY_TTL': 3600,
'ASSETS_PERM_CACHE_ENABLE': False, 'ASSETS_PERM_CACHE_ENABLE': False,
@ -284,6 +293,8 @@ class DynamicConfig:
] ]
if self.get('AUTH_LDAP'): if self.get('AUTH_LDAP'):
backends.insert(0, 'authentication.backends.ldap.LDAPAuthorizationBackend') backends.insert(0, 'authentication.backends.ldap.LDAPAuthorizationBackend')
if self.static_config.get('AUTH_CAS'):
backends.insert(0, 'authentication.backends.cas.CASBackend')
if self.static_config.get('AUTH_OPENID'): if self.static_config.get('AUTH_OPENID'):
backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend') backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend')
backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend') backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend')

View File

@ -7,6 +7,6 @@ __all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG', 'DYNAMIC']
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
VERSION = '1.5.6' VERSION = '1.5.7'
CONFIG = ConfigManager.load_user_config() CONFIG = ConfigManager.load_user_config()
DYNAMIC = ConfigManager.get_dynamic_config(CONFIG) DYNAMIC = ConfigManager.get_dynamic_config(CONFIG)

View File

@ -13,7 +13,7 @@ def jumpserver_processor(request):
'LOGO_TEXT_URL': static('img/logo_text.png'), 'LOGO_TEXT_URL': static('img/logo_text.png'),
'LOGIN_IMAGE_URL': static('img/login_image.png'), 'LOGIN_IMAGE_URL': static('img/login_image.png'),
'FAVICON_URL': static('img/facio.ico'), 'FAVICON_URL': static('img/facio.ico'),
'JMS_TITLE': 'Jumpserver', 'JMS_TITLE': 'JumpServer',
'VERSION': settings.VERSION, 'VERSION': settings.VERSION,
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2020', 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2020',
'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION,

View File

@ -64,7 +64,22 @@ RADIUS_SERVER = CONFIG.RADIUS_SERVER
RADIUS_PORT = CONFIG.RADIUS_PORT RADIUS_PORT = CONFIG.RADIUS_PORT
RADIUS_SECRET = CONFIG.RADIUS_SECRET RADIUS_SECRET = CONFIG.RADIUS_SECRET
# CAS Auth
AUTH_CAS = CONFIG.AUTH_CAS
CAS_SERVER_URL = CONFIG.CAS_SERVER_URL
CAS_VERIFY_SSL_CERTIFICATE = False
CAS_LOGIN_URL_NAME = "authentication:cas:cas-login"
CAS_LOGOUT_URL_NAME = "authentication:cas:cas-logout"
CAS_LOGIN_MSG = None
CAS_LOGGED_MSG = None
CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY
CAS_VERSION = CONFIG.CAS_VERSION
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
# Other setting
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
AUTHENTICATION_BACKENDS = DYNAMIC.AUTHENTICATION_BACKENDS

View File

@ -51,6 +51,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'drf_yasg', 'drf_yasg',
'django_cas_ng',
'channels', 'channels',
'django_filters', 'django_filters',
'bootstrap3', 'bootstrap3',
@ -75,6 +76,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware', 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware',
'django_cas_ng.middleware.CASMiddleware',
'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware', 'jumpserver.middleware.RequestMiddleware',
@ -220,8 +222,6 @@ EMAIL_USE_SSL = DYNAMIC.EMAIL_USE_SSL
EMAIL_USE_TLS = DYNAMIC.EMAIL_USE_TLS EMAIL_USE_TLS = DYNAMIC.EMAIL_USE_TLS
AUTHENTICATION_BACKENDS = DYNAMIC.AUTHENTICATION_BACKENDS
# Custom User Auth model # Custom User Auth model
AUTH_USER_MODEL = 'users.User' AUTH_USER_MODEL = 'users.User'

View File

@ -82,5 +82,6 @@ USER_GUIDE_URL = DYNAMIC.USER_GUIDE_URL
HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
LOGIN_LOG_KEEP_DAYS = DYNAMIC.LOGIN_LOG_KEEP_DAYS LOGIN_LOG_KEEP_DAYS = DYNAMIC.LOGIN_LOG_KEEP_DAYS
TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS
ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD

View File

@ -53,6 +53,7 @@ SWAGGER_SETTINGS = {
'in': 'header' 'in': 'header'
} }
}, },
'DEFAULT_INFO': 'jumpserver.views.swagger.api_info',
} }

View File

@ -96,12 +96,16 @@ LOGGING = {
'handlers': ['syslog'], 'handlers': ['syslog'],
'level': 'INFO' 'level': 'INFO'
}, },
# 'django.db': {
# 'handlers': ['console', 'file'],
# 'level': 'DEBUG'
# }
} }
} }
if os.environ.get("DEBUG_DB"):
LOGGING['loggers']['django.db'] = {
'handlers': ['console', 'file'],
'level': 'DEBUG'
}
SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE
if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2:

View File

@ -1,30 +1,214 @@
import datetime from django.core.cache import cache
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count from django.db.models import Count, Max
from django.shortcuts import redirect from django.shortcuts import redirect
from users.models import User from users.models import User
from assets.models import Asset from assets.models import Asset
from terminal.models import Session from terminal.models import Session
from orgs.utils import current_org from orgs.utils import current_org
from common.permissions import PermissionsMixin, IsValidUser from common.permissions import PermissionsMixin, IsValidUser
from common.utils import timeit, lazyproperty
__all__ = ['IndexView'] __all__ = ['IndexView']
class IndexView(PermissionsMixin, TemplateView): class MonthLoginMetricMixin:
@lazyproperty
def session_month(self):
month_ago = timezone.now() - timezone.timedelta(days=30)
session_month = Session.objects.filter(date_start__gt=month_ago)
return session_month
@lazyproperty
def session_month_dates(self):
return self.session_month.dates('date_start', 'day')
def get_month_day_metrics(self):
month_str = [
d.strftime('%m-%d') for d in self.session_month_dates
] or ['0']
return month_str
@staticmethod
def get_cache_key(date, tp):
date_str = date.strftime("%Y%m%d")
key = "SESSION_MONTH_{}_{}".format(tp, date_str)
return key
def __get_data_from_cache(self, date, tp):
if date == timezone.now().date():
return None
cache_key = self.get_cache_key(date, tp)
count = cache.get(cache_key)
return count
def __set_data_to_cache(self, date, tp, count):
cache_key = self.get_cache_key(date, tp)
cache.set(cache_key, count, 3600*24*7)
@lazyproperty
def user_disabled_total(self):
return current_org.get_org_members().filter(is_active=False).count()
@lazyproperty
def asset_disabled_total(self):
return Asset.objects.filter(is_active=False).count()
def get_date_login_count(self, date):
tp = "LOGIN"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
count = Session.objects.filter(date_start__date=date).count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_login_metrics(self):
data = []
for d in self.session_month_dates:
count = self.get_date_login_count(d)
data.append(count)
if len(data) == 0:
data = [0]
return data
def get_date_user_count(self, date):
tp = "USER"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
count = Session.objects.filter(date_start__date=date)\
.values('user').distinct().count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_active_user_metrics(self):
data = []
for d in self.session_month_dates:
count = self.get_date_user_count(d)
data.append(count)
return data
def get_date_asset_count(self, date):
tp = "ASSET"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
count = Session.objects.filter(date_start__date=date) \
.values('asset').distinct().count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_active_asset_metrics(self):
data = []
for d in self.session_month_dates:
count = self.get_date_asset_count(d)
data.append(count)
return data
@lazyproperty
def month_active_user_total(self):
count = self.session_month.values('user').distinct().count()
return count
@lazyproperty
def month_inactive_user_total(self):
total = current_org.get_org_members().count()
active = self.month_active_user_total
count = total - active
if count < 0:
count = 0
return count
@lazyproperty
def month_active_asset_total(self):
return self.session_month.values('asset').distinct().count()
@lazyproperty
def month_inactive_asset_total(self):
total = Asset.objects.all().count()
active = self.month_active_asset_total
count = total - active
if count < 0:
count = 0
return count
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'month_str': self.get_month_day_metrics(),
'month_total_visit_count': self.get_month_login_metrics(),
'month_user': self.get_month_active_user_metrics(),
'mouth_asset': self.get_month_active_asset_metrics(),
'month_user_active': self.month_active_user_total,
'month_user_inactive': self.month_inactive_user_total,
'month_user_disabled': self.user_disabled_total,
'month_asset_active': self.month_active_asset_total,
'month_asset_inactive': self.month_inactive_asset_total,
'month_asset_disabled': self.asset_disabled_total,
})
return context
class WeekSessionMetricMixin:
session_week = None
@lazyproperty
def session_week(self):
week_ago = timezone.now() - timezone.timedelta(weeks=1)
session_week = Session.objects.filter(date_start__gt=week_ago)
return session_week
def get_top5_user_a_week(self):
users = self.session_week.values('user') \
.annotate(total=Count('user')) \
.order_by('-total')[:5]
return users
def get_week_login_user_count(self):
return self.session_week.values('user').distinct().count()
def get_week_login_asset_count(self):
return self.session_week.count()
def get_week_top10_assets(self):
assets = self.session_week.values("asset")\
.annotate(total=Count("asset"))\
.annotate(last=Max("date_start")).order_by("-total")[:10]
return assets
def get_week_top10_users(self):
users = self.session_week.values("user") \
.annotate(total=Count("user")) \
.annotate(last=Max("date_start")).order_by("-total")[:10]
return users
def get_last10_sessions(self):
sessions = self.session_week.order_by('-date_start')[:10]
for session in sessions:
session.avatar_url = User.get_avatar_url("")
return sessions
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'user_visit_count_weekly': self.get_week_login_user_count(),
'asset_visit_count_weekly': self.get_week_login_asset_count(),
'user_visit_count_top_five': self.get_top5_user_a_week(),
'last_login_ten': self.get_last10_sessions(),
'week_asset_hot_ten': self.get_week_top10_assets(),
'week_user_hot_ten': self.get_week_top10_users(),
})
return context
class IndexView(PermissionsMixin, MonthLoginMetricMixin, WeekSessionMetricMixin, TemplateView):
template_name = 'index.html' template_name = 'index.html'
permission_classes = [IsValidUser] permission_classes = [IsValidUser]
session_week = None
session_month = None
session_month_dates = []
session_month_dates_archive = []
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return self.handle_no_permission() return self.handle_no_permission()
@ -42,141 +226,21 @@ class IndexView(PermissionsMixin, TemplateView):
@staticmethod @staticmethod
def get_online_user_count(): def get_online_user_count():
return len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True))) count = Session.objects.filter(is_finished=False)\
.values_list('user', flat=True).distinct().count()
return count
@staticmethod @staticmethod
def get_online_session_count(): def get_online_session_count():
return Session.objects.filter(is_finished=False).count() return Session.objects.filter(is_finished=False).count()
def get_top5_user_a_week(self):
return self.session_week.values('user').annotate(total=Count('user')).order_by('-total')[:5]
def get_week_login_user_count(self):
return self.session_week.values('user').distinct().count()
def get_week_login_asset_count(self):
return self.session_week.count()
def get_month_day_metrics(self):
month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0']
return month_str
def get_month_login_metrics(self):
data = []
time_min = datetime.datetime.min.time()
time_max = datetime.datetime.max.time()
for d in self.session_month_dates:
ds = datetime.datetime.combine(d, time_min).replace(tzinfo=timezone.get_current_timezone())
de = datetime.datetime.combine(d, time_max).replace(tzinfo=timezone.get_current_timezone())
data.append(self.session_month.filter(date_start__range=(ds, de)).count())
return data
def get_month_active_user_metrics(self):
if self.session_month_dates_archive:
return [q.values('user').distinct().count()
for q in self.session_month_dates_archive]
else:
return [0]
def get_month_active_asset_metrics(self):
if self.session_month_dates_archive:
return [q.values('asset').distinct().count()
for q in self.session_month_dates_archive]
else:
return [0]
def get_month_active_user_total(self):
return self.session_month.values('user').distinct().count()
def get_month_inactive_user_total(self):
count = current_org.get_org_members().count() - self.get_month_active_user_total()
if count < 0:
count = 0
return count
def get_month_active_asset_total(self):
return self.session_month.values('asset').distinct().count()
def get_month_inactive_asset_total(self):
count = Asset.objects.all().count() - self.get_month_active_asset_total()
if count < 0:
count = 0
return count
@staticmethod
def get_user_disabled_total():
return current_org.get_org_members().filter(is_active=False).count()
@staticmethod
def get_asset_disabled_total():
return Asset.objects.filter(is_active=False).count()
def get_week_top10_asset(self):
assets = list(self.session_week.values('asset').annotate(total=Count('asset')).order_by('-total')[:10])
for asset in assets:
last_login = self.session_week.filter(asset=asset["asset"]).order_by('date_start').last()
asset['last'] = last_login
return assets
def get_week_top10_user(self):
users = list(self.session_week.values('user').annotate(
total=Count('asset')).order_by('-total')[:10])
for user in users:
last_login = self.session_week.filter(user=user["user"]).order_by('date_start').last()
user['last'] = last_login
return users
def get_last10_sessions(self):
sessions = self.session_week.order_by('-date_start')[:10]
for session in sessions:
try:
session.avatar_url = User.objects.get(username=session.user).avatar_url()
except User.DoesNotExist:
session.avatar_url = User.objects.first().avatar_url()
return sessions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
week_ago = timezone.now() - timezone.timedelta(weeks=1) context = super().get_context_data(**kwargs)
month_ago = timezone.now() - timezone.timedelta(days=30) context.update({
self.session_week = Session.objects.filter(date_start__gt=week_ago)
self.session_month = Session.objects.filter(date_start__gt=month_ago)
self.session_month_dates = self.session_month.dates('date_start', 'day')
self.session_month_dates_archive = []
time_min = datetime.datetime.min.time()
time_max = datetime.datetime.max.time()
for d in self.session_month_dates:
ds = datetime.datetime.combine(d, time_min).replace(
tzinfo=timezone.get_current_timezone())
de = datetime.datetime.combine(d, time_max).replace(
tzinfo=timezone.get_current_timezone())
self.session_month_dates_archive.append(
self.session_month.filter(date_start__range=(ds, de)))
context = {
'assets_count': self.get_asset_count(), 'assets_count': self.get_asset_count(),
'users_count': self.get_user_count(), 'users_count': self.get_user_count(),
'online_user_count': self.get_online_user_count(), 'online_user_count': self.get_online_user_count(),
'online_asset_count': self.get_online_session_count(), 'online_asset_count': self.get_online_session_count(),
'user_visit_count_weekly': self.get_week_login_user_count(),
'asset_visit_count_weekly': self.get_week_login_asset_count(),
'user_visit_count_top_five': self.get_top5_user_a_week(),
'month_str': self.get_month_day_metrics(),
'month_total_visit_count': self.get_month_login_metrics(),
'month_user': self.get_month_active_user_metrics(),
'mouth_asset': self.get_month_active_asset_metrics(),
'month_user_active': self.get_month_active_user_total(),
'month_user_inactive': self.get_month_inactive_user_total(),
'month_user_disabled': self.get_user_disabled_total(),
'month_asset_active': self.get_month_active_asset_total(),
'month_asset_inactive': self.get_month_inactive_asset_total(),
'month_asset_disabled': self.get_asset_disabled_total(),
'week_asset_hot_ten': self.get_week_top10_asset(),
'last_login_ten': self.get_last10_sessions(),
'week_user_hot_ten': self.get_week_top10_user(),
'app': _("Dashboard"), 'app': _("Dashboard"),
} })
return context
kwargs.update(context)
return super(IndexView, self).get_context_data(**kwargs)

View File

@ -49,6 +49,16 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema):
return fields return fields
api_info = openapi.Info(
title="JumpServer API Docs",
default_version='v1',
description="JumpServer Restful api docs",
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
license=openapi.License(name="GPLv2 License"),
)
def get_swagger_view(version='v1'): def get_swagger_view(version='v1'):
from ..urls import api_v1, api_v2 from ..urls import api_v1, api_v2
from django.urls import path, include from django.urls import path, include
@ -65,14 +75,7 @@ def get_swagger_view(version='v1'):
else: else:
patterns = api_v1_patterns patterns = api_v1_patterns
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( api_info,
title="Jumpserver API Docs",
default_version=version,
description="Jumpserver Restful api docs",
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
license=openapi.License(name="GPLv2 License"),
),
public=True, public=True,
patterns=patterns, patterns=patterns,
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.AllowAny,),

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More