* [Update] 统一url地址

* [Update] 修改api

* [Update] 使用规范的签名

* [Update] 修改url

* [Update] 修改swagger

* [Update] 添加serializer class避免报错

* [Update] 修改token

* [Update] 支持api key

* [Update] 支持生成api key

* [Update] 修改api重定向

* [Update] 修改翻译

* [Update] 添加说明文档

* [Update] 修复浏览器关闭后session不失效的问题

* [Update] 修改一些内容

* [Update] 修改 jms脚本

* [Update] 修改重定向

* [Update] 修改搜索trim

* [Update] 修改搜索trim

* [Update] 添加sys log

* [Bugfix] 修改登陆错误

* [Update] 优化User操作private_token的接口 (#3091)

* [Update] 优化User操作private_token的接口

* [Update] 优化User操作private_token的接口 2

* [Bugfix] 解决授权了一个节点,当移动节点后,被移动的节点下的资产会放到未分组节点下的问题

* [Update] 升级jquery

* [Update] 默认使用page

* [Update] 修改使用Orgmodel view set

* [Update] 支持 nv的硬盘 https://github.com/jumpserver/jumpserver/issues/1804

* [UPdate] 解决命令执行宽度问题

* [Update] 优化节点

* [Update] 修改nodes过多时创建比较麻烦

* [Update] 修改导入

* [Update] 节点获取更新

* [Update] 修改nodes

* [Update] nodes显示full value

* [Update] 统一使用nodes select2 函数

* [Update] 修改磁盘大小小数

* [Update] 修改 Node service

* [Update] 优化授权节点

* [Update] 修改 node permission

* [Update] 修改asset permission

* [Stash]

* [Update] 修改node assets api

* [Update] 修改tree service,支持资产数量

* [Update] 修改暂时完成

* [Update] 修改一些bug
pull/3146/head
老广 2019-08-21 20:27:21 +08:00 committed by GitHub
parent fe6f7bcfc1
commit 164f48e131
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 2620 additions and 2331 deletions

View File

@ -3,9 +3,8 @@
from rest_framework import generics
from rest_framework.pagination import LimitOffsetPagination
from rest_framework_bulk import BulkModelViewSet
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdmin, IsAppUser
from ..models import RemoteApp
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
@ -16,13 +15,12 @@ __all__ = [
]
class RemoteAppViewSet(BulkModelViewSet):
class RemoteAppViewSet(OrgBulkModelViewSet):
filter_fields = ('name',)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
queryset = RemoteApp.objects.all()
serializer_class = RemoteAppSerializer
pagination_class = LimitOffsetPagination
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):

View File

@ -4,7 +4,7 @@
from django.utils.translation import ugettext as _
from django import forms
from orgs.mixins import OrgModelForm
from orgs.mixins.forms import OrgModelForm
from assets.models import SystemUser
from ..models import RemoteApp

View File

@ -5,7 +5,7 @@ import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from orgs.mixins.models import OrgModelMixin
from common.fields.model import EncryptJsonDictTextField
from .. import const

View File

@ -5,7 +5,7 @@
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .. import const
from ..models import RemoteApp

View File

@ -1,20 +1,24 @@
# coding:utf-8
#
from django.urls import path
from django.urls import path, re_path
from rest_framework_bulk.routes import BulkRouter
from common import api as capi
from .. import api
app_name = 'applications'
router = BulkRouter()
router.register(r'remote-app', api.RemoteAppViewSet, 'remote-app')
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/',
api.RemoteAppConnectionInfoApi.as_view(),
name='remote-app-connection-info')
]
old_version_urlpatterns = [
re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api)
]
urlpatterns += router.urls
urlpatterns += router.urls + old_version_urlpatterns

View File

@ -17,8 +17,7 @@ from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from orgs.mixins.api import OrgBulkModelViewSet
from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger
@ -36,7 +35,7 @@ __all__ = [
]
class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
class AdminUserViewSet(OrgBulkModelViewSet):
"""
Admin user api set, for add,delete,update,list,retrieve resource
"""
@ -46,11 +45,6 @@ class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
queryset = AdminUser.objects.all()
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
class AdminUserAuthApi(generics.UpdateAPIView):
@ -98,7 +92,6 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields

View File

@ -1,27 +1,17 @@
# -*- coding: utf-8 -*-
#
import uuid
import random
from rest_framework import generics
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
from rest_framework.pagination import LimitOffsetPagination
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db.models import Q
from common.mixins import IDInCacheFilterMixin, ApiMessageMixin
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from orgs.mixins import OrgBulkModelViewSet
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Asset, AdminUser, Node
from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \
@ -31,9 +21,9 @@ from ..utils import LabelFilter
logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetListUpdateApi',
'AssetViewSet',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
'AssetGatewayApi', 'AssetBulkUpdateSelectAPI'
'AssetGatewayApi',
]
@ -46,7 +36,6 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet):
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,)
success_message = _("%(hostname)s was %(action)s successfully")
@ -73,19 +62,21 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet):
node = get_object_or_404(Node, id=node_id)
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
# 当前节点是顶层节点, 并且仅显示直接资产
if node.is_root() and show_current_asset:
queryset = queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
)
).distinct()
# 当前节点是顶层节点,显示所有资产
elif node.is_root() and not show_current_asset:
pass
return queryset
# 当前节点不是鼎城节点,只显示直接资产
elif not node.is_root() and show_current_asset:
queryset = queryset.filter(nodes=node)
else:
queryset = queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
)
return queryset.distinct()
children = node.get_all_children(with_self=True)
queryset = queryset.filter(nodes__in=children).distinct()
return queryset
def filter_admin_user_id(self, queryset):
admin_user_id = self.request.query_params.get('admin_user_id')
@ -102,30 +93,6 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet):
return queryset
class AssetListUpdateApi(IDInCacheFilterMixin, ListBulkCreateUpdateDestroyAPIView):
"""
Asset bulk update api
"""
queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer
permission_classes = (IsOrgAdmin,)
class AssetBulkUpdateSelectAPI(APIView):
permission_classes = (IsOrgAdmin,)
def post(self, request, *args, **kwargs):
assets_id = request.data.get('assets_id', '')
if assets_id:
spm = uuid.uuid4().hex
key = CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm)
cache.set(key, assets_id, 300)
url = reverse_lazy('assets:asset-bulk-update') + '?spm=%s' % spm
return Response({'url': url})
error = _('Please select assets that need to be updated')
return Response({'error': error}, status=400)
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
"""
Refresh asset hardware info

View File

@ -2,11 +2,11 @@
#
from rest_framework.response import Response
from rest_framework import viewsets, status, generics
from rest_framework.pagination import LimitOffsetPagination
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 common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
from common.utils import get_object_or_none, get_logger
@ -53,7 +53,6 @@ class AssetUserSearchBackend(filters.BaseFilterBackend):
class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
pagination_class = LimitOffsetPagination
serializer_class = serializers.AssetUserSerializer
permission_classes = [IsOrgAdminOrAppUser]
http_method_names = ['get', 'post']
@ -67,6 +66,9 @@ class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
AssetUserFilterBackend, AssetUserSearchBackend,
)
def allow_bulk_destroy(self, qs, filtered):
return False
def get_queryset(self):
# 尽可能先返回更少的数据
username = self.request.GET.get('username')
@ -115,14 +117,6 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView):
serializer_class = serializers.AssetUserAuthInfoSerializer
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
status_code = status.HTTP_200_OK
if not instance:
status_code = status.HTTP_400_BAD_REQUEST
return Response(serializer.data, status=status_code)
def get_object(self):
query_params = self.request.query_params
username = query_params.get('username')
@ -133,8 +127,7 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView):
manger = AssetUserManager()
instance = manger.get(username, asset, prefer=prefer)
except Exception as e:
logger.error(e, exc_info=True)
return None
raise Http404("Not found")
else:
return instance

View File

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
#
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.shortcuts import get_object_or_404
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdmin
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
@ -13,21 +12,19 @@ from .. import serializers
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(BulkModelViewSet):
class CommandFilterViewSet(OrgBulkModelViewSet):
filter_fields = ("name",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
queryset = CommandFilter.objects.all()
serializer_class = serializers.CommandFilterSerializer
pagination_class = LimitOffsetPagination
class CommandFilterRuleViewSet(BulkModelViewSet):
class CommandFilterRuleViewSet(OrgBulkModelViewSet):
filter_fields = ("content",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
fpk = self.kwargs.get('filter_pk')

View File

@ -1,13 +1,11 @@
# ~*~ coding: utf-8 ~*~
from rest_framework_bulk import BulkModelViewSet
from rest_framework.views import APIView, Response
from rest_framework.pagination import LimitOffsetPagination
from django.views.generic.detail import SingleObjectMixin
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Domain, Gateway
from .. import serializers
@ -16,11 +14,10 @@ logger = get_logger(__file__)
__all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
class DomainViewSet(BulkModelViewSet):
class DomainViewSet(OrgBulkModelViewSet):
queryset = Domain.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.DomainSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
@ -37,13 +34,12 @@ class DomainViewSet(BulkModelViewSet):
return super().get_permissions()
class GatewayViewSet(BulkModelViewSet):
class GatewayViewSet(OrgBulkModelViewSet):
filter_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = filter_fields
queryset = Gateway.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.GatewaySerializer
pagination_class = LimitOffsetPagination
class GatewayTestConnectionApi(SingleObjectMixin, APIView):

View File

@ -13,11 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from rest_framework.pagination import LimitOffsetPagination
from django.db.models import Count
from common.utils import get_logger
from orgs.mixins import OrgBulkModelViewSet
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdmin
from ..models import Label
from .. import serializers
@ -32,7 +31,6 @@ class LabelViewSet(OrgBulkModelViewSet):
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer
pagination_class = LimitOffsetPagination
def list(self, request, *args, **kwargs):
if request.query_params.get("distinct"):

View File

@ -13,7 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from rest_framework import generics, mixins, viewsets
import time
from rest_framework import generics
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
@ -22,11 +24,11 @@ from django.shortcuts import get_object_or_404
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from orgs.mixins.api import OrgModelViewSet
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util
from .. import serializers
from ..utils import NodeUtil
logger = get_logger(__file__)
@ -39,29 +41,25 @@ __all__ = [
]
class NodeViewSet(viewsets.ModelViewSet):
filter_fields = ('value', 'key', )
search_fields = filter_fields
class NodeViewSet(OrgModelViewSet):
filter_fields = ('value', 'key', 'id')
search_fields = ('value', )
queryset = Node.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
# 仅支持根节点指直接创建子节点下的节点需要通过children接口创建
def perform_create(self, serializer):
child_key = Node.root().get_next_child_key()
serializer.validated_data["key"] = child_key
serializer.save()
def update(self, request, *args, **kwargs):
def perform_update(self, serializer):
node = self.get_object()
if node.is_root():
node_value = node.value
post_value = request.data.get('value')
if node_value != post_value:
return Response(
{"msg": _("You can't update the root node name")},
status=400
)
return super().update(request, *args, **kwargs)
if node.is_root() and node.value != serializer.validated_data['value']:
msg = _("You can't update the root node name")
raise ValidationError({"error": msg})
return super().perform_update(serializer)
class NodeListAsTreeApi(generics.ListAPIView):
@ -79,21 +77,72 @@ class NodeListAsTreeApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
@staticmethod
def to_tree_queryset(queryset):
queryset = [node.as_tree_node() for node in queryset]
return queryset
def get_queryset(self):
queryset = Node.objects.all()
util = NodeUtil()
nodes = util.get_nodes_by_queryset(queryset)
queryset = [node.as_tree_node() for node in nodes]
return queryset
@staticmethod
def refresh_nodes(queryset):
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.to_tree_queryset(queryset)
return queryset
class NodeChildrenAsTreeApi(generics.ListAPIView):
class NodeChildrenApi(generics.ListCreateAPIView):
queryset = Node.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
instance = None
def initial(self, request, *args, **kwargs):
self.instance = self.get_object()
return super().initial(request, *args, **kwargs)
def perform_create(self, serializer):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
key = self.request.query_params.get("key")
if not pk and not key:
node = Node.root()
return node
if pk:
node = get_object_or_404(Node, pk=pk)
else:
node = get_object_or_404(Node, key=key)
return node
def get_queryset(self):
query_all = self.request.query_params.get("all", "0") == "all"
if not self.instance:
return Node.objects.none()
if self.instance.is_root():
with_self = True
else:
with_self = False
if query_all:
queryset = self.instance.get_all_children(with_self=with_self)
else:
queryset = self.instance.get_children(with_self=with_self)
return queryset
class NodeChildrenAsTreeApi(NodeChildrenApi):
"""
节点子节点作为树返回
[
@ -106,39 +155,26 @@ class NodeChildrenAsTreeApi(generics.ListAPIView):
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
node = None
is_root = False
http_method_names = ['get']
def get_queryset(self):
self.check_need_refresh_nodes()
node_key = self.request.query_params.get('key')
util = NodeUtil()
# 是否包含自己
with_self = False
if not node_key:
node_key = Node.root().key
with_self = True
self.node = util.get_node_by_key(node_key)
queryset = self.node.get_children(with_self=with_self)
queryset = super().get_queryset()
queryset = [node.as_tree_node() for node in queryset]
queryset = self.add_assets_if_need(queryset)
queryset = sorted(queryset)
return queryset
def filter_assets(self, queryset):
def add_assets_if_need(self, queryset):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets:
return queryset
assets = self.node.get_assets().only(
"id", "hostname", "ip", 'platform', "os", "org_id", "protocols",
assets = self.instance.get_assets().only(
"id", "hostname", "ip", 'platform', "os",
"org_id", "protocols",
)
for asset in assets:
queryset.append(asset.as_tree_node(self.node))
return queryset
def filter_queryset(self, queryset):
queryset = self.filter_assets(queryset)
queryset.append(asset.as_tree_node(self.instance))
return queryset
def check_need_refresh_nodes(self):
@ -146,59 +182,6 @@ class NodeChildrenAsTreeApi(generics.ListAPIView):
Node.refresh_nodes()
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
queryset = Node.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
instance = None
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
instance = self.get_object()
if not request.data.get("value"):
request.data["value"] = instance.get_next_child_preset_name()
return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
instance = self.get_object()
value = request.data.get("value")
_id = request.data.get('id') or None
values = [child.value for child in instance.get_children()]
if value in values:
raise ValidationError(
'The same level node name cannot be the same'
)
node = instance.create_child(value=value, _id=_id)
return Response(self.serializer_class(instance=node).data, status=201)
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
if not pk:
node = Node.root()
else:
node = get_object_or_404(Node, pk=pk)
return node
def get_queryset(self):
queryset = []
query_all = self.request.query_params.get("all")
node = self.get_object()
if node is None:
node = Node.root()
node.assets__count = node.get_all_assets().count()
queryset.append(node)
if query_all:
children = node.get_all_children()
else:
children = node.get_children()
queryset.extend(list(children))
return queryset
class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer

View File

@ -16,18 +16,17 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.serializers import CeleryTaskSerializer
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.mixins import IDInCacheFilterMixin
from orgs.mixins import OrgBulkModelViewSet
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import SystemUser, Asset
from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
test_system_user_connectivity_a_asset
from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
push_system_user_a_asset_manual, test_system_user_connectivity_a_asset,
)
logger = get_logger(__file__)
@ -49,7 +48,6 @@ class SystemUserViewSet(OrgBulkModelViewSet):
queryset = SystemUser.objects.all()
serializer_class = serializers.SystemUserSerializer
permission_classes = (IsOrgAdminOrAppUser,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
@ -92,6 +90,7 @@ class SystemUserPushApi(generics.RetrieveAPIView):
"""
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = CeleryTaskSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
@ -108,6 +107,7 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
"""
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = CeleryTaskSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
@ -118,7 +118,6 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields

View File

@ -4,7 +4,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger
from orgs.mixins import OrgModelForm
from orgs.mixins.forms import OrgModelForm
from ..models import Asset, Node
@ -29,9 +29,14 @@ class ProtocolForm(forms.Form):
class AssetCreateForm(OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.data:
return
nodes_field = self.fields['nodes']
nodes_field.choices = ((n.id, n.full_value) for n in
Node.get_queryset())
if self.instance:
nodes_field.choices = ((n.id, n.full_value) for n in
self.instance.nodes.all())
else:
nodes_field.choices = []
class Meta:
model = Asset
@ -42,7 +47,7 @@ class AssetCreateForm(OrgModelForm):
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Nodes')
'class': 'nodes-select2', 'data-placeholder': _('Nodes')
}),
'admin_user': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Admin user')
@ -68,6 +73,17 @@ class AssetCreateForm(OrgModelForm):
class AssetUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.data:
return
nodes_field = self.fields['nodes']
if self.instance:
nodes_field.choices = ((n.id, n.full_value) for n in
self.instance.nodes.all())
else:
nodes_field.choices = []
class Meta:
model = Asset
fields = [
@ -77,7 +93,7 @@ class AssetUpdateForm(OrgModelForm):
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Node')
'class': 'nodes-select2', 'data-placeholder': _('Node')
}),
'admin_user': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Admin user')

View File

@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
import re
from orgs.mixins import OrgModelForm
from orgs.mixins.forms import OrgModelForm
from ..models import CommandFilter, CommandFilterRule
__all__ = ['CommandFilterForm', 'CommandFilterRuleForm']

View File

@ -3,7 +3,7 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from orgs.mixins import OrgModelForm
from orgs.mixins.forms import OrgModelForm
from ..models import Domain, Asset, Gateway
from .user import PasswordAndKeyAuthForm

View File

@ -4,7 +4,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
from orgs.mixins import OrgModelForm
from orgs.mixins.forms import OrgModelForm
from ..models import AdminUser, SystemUser
logger = get_logger(__file__)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-07-24 12:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0036_auto_20190716_1535'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='_become_pass',
field=models.CharField(blank=True, default='', max_length=128),
),
]

View File

@ -13,7 +13,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from .utils import Connectivity
from orgs.mixins import OrgModelMixin, OrgManager
from orgs.mixins.models import OrgModelMixin, OrgManager
__all__ = ['Asset', 'ProtocolsMixin']
logger = logging.getLogger(__name__)
@ -345,7 +345,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
else:
_nodes = [Node.default_node()]
asset.nodes.set(_nodes)
asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)]
logger.debug('Generate fake asset : %s' % asset.ip)
except IntegrityError:
print('Error continue')

View File

@ -4,7 +4,7 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgManager
from orgs.mixins.models import OrgManager
from .base import AssetUser
__all__ = ['AuthBook']

View File

@ -15,7 +15,7 @@ from common.utils import (
)
from common.validators import alphanumeric
from common import fields
from orgs.mixins import OrgModelMixin
from orgs.mixins.models import OrgModelMixin
from .utils import private_key_validator, Connectivity
signer = get_signer()

View File

@ -7,7 +7,7 @@ from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from orgs.mixins.models import OrgModelMixin
__all__ = [

View File

@ -9,7 +9,7 @@ import paramiko
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from orgs.mixins.models import OrgModelMixin
from .base import AssetUser
__all__ = ['Domain', 'Gateway']

View File

@ -4,7 +4,7 @@
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from orgs.mixins.models import OrgModelMixin
class Label(OrgModelMixin):

View File

@ -2,6 +2,7 @@
#
import uuid
import re
import time
from django.db import models, transaction
from django.db.models import Q
@ -9,10 +10,11 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.cache import cache
from orgs.mixins import OrgModelMixin, OrgManager
from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import set_current_org, get_current_org
from orgs.models import Organization
__all__ = ['Node']
@ -21,58 +23,81 @@ class NodeQuerySet(models.QuerySet):
raise PermissionError("Bulk delete node deny")
class TreeMixin:
time_tree_updated = None
time_tree_updated_cache_key = 'NODE_TREE_CREATED_AT'
tree_cache_time = 3600
_tree_service = None
@classmethod
def tree(cls):
# Todo: 有待优化, 因为每次刷新都会导致其他节点的tree失效
# Todo: ungroup node
# TOdo: 游离的资产,在树上显示的数量不对
# Todo: api key页面有bug
from ..utils import TreeService
cache_updated_time = cls.get_cache_time()
if not cls.time_tree_updated or \
cache_updated_time != cls.time_tree_updated:
t = TreeService.new()
cls.update_cache_tree(t)
return t
return cls._tree_service
@classmethod
def get_cache_time(cls):
return cache.get(cls.time_tree_updated_cache_key)
@classmethod
def update_cache_tree(cls, t):
cls._tree_service = t
now = time.time()
cls.time_tree_updated = now
cache.set(cls.time_tree_updated_cache_key, now, cls.tree_cache_time)
@classmethod
def expire_cache_tree(cls):
cache.delete(cls.time_tree_updated_cache_key)
@classmethod
def refresh_tree(cls):
cls.expire_cache_tree()
@property
def _tree(self):
return self.__class__.tree()
class FamilyMixin:
_parents = None
_children = None
_all_children = None
__parents = None
__children = None
__all_children = None
is_node = True
@property
def children(self):
if self._children:
return self._children
pattern = r'^{0}:[0-9]+$'.format(self.key)
return Node.objects.filter(key__regex=pattern)
@children.setter
def children(self, value):
self._children = value
return self.get_children(with_self=False)
@property
def all_children(self):
if self._all_children:
return self._all_children
pattern = r'^{0}:'.format(self.key)
return Node.objects.filter(
key__regex=pattern
)
return self.get_all_children(with_self=False)
def get_children(self, with_self=False):
children = list(self.children)
pattern = r'^{0}:[0-9]+$'.format(self.key)
if with_self:
children.append(self)
return children
pattern += r'|^{0}$'.format(self.key)
return Node.objects.filter(key__regex=pattern)
def get_all_children(self, with_self=False):
children = self.all_children
pattern = r'^{0}:'.format(self.key)
if with_self:
children = list(children)
children.append(self)
pattern += r'|^{0}$'.format(self.key)
children = Node.objects.filter(key__regex=pattern)
return children
@property
def parents(self):
if self._parents:
return self._parents
ancestor_keys = self.get_ancestor_keys()
ancestor = Node.objects.filter(
key__in=ancestor_keys
).order_by('key')
return ancestor
@parents.setter
def parents(self, value):
self._parents = value
return self.get_ancestor(with_self=False)
def get_ancestor(self, with_self=False):
parents = self.parents
@ -83,15 +108,10 @@ class FamilyMixin:
@property
def parent(self):
if self._parents:
return self._parents[0]
if self.is_root():
return self
try:
parent = Node.objects.get(key=self.parent_key)
return parent
except Node.DoesNotExist:
return Node.root()
parent_key = self.parent_key
return Node.objects.get(key=parent_key)
@parent.setter
def parent(self, parent):
@ -107,7 +127,7 @@ class FamilyMixin:
child.save()
self.save()
def get_sibling(self, with_self=False):
def get_siblings(self, with_self=False):
key = ':'.join(self.key.split(':')[:-1])
pattern = r'^{}:[0-9]+$'.format(key)
sibling = Node.objects.filter(
@ -133,12 +153,11 @@ class FamilyMixin:
return parent_keys
def is_children(self, other):
pattern = re.compile(r'^{0}:[0-9]+$'.format(self.key))
return pattern.match(other.key)
pattern = r'^{0}:[0-9]+$'.format(self.key)
return re.match(pattern, other.key)
def is_parent(self, other):
pattern = re.compile(r'^{0}:[0-9]+$'.format(other.key))
return pattern.match(self.key)
return other.is_children(self)
@property
def parent_key(self):
@ -158,46 +177,27 @@ class FamilyMixin:
class FullValueMixin:
_full_value_cache_key = '_NODE_VALUE_{}'
_full_value = ''
_full_value = None
key = ''
@property
def full_value(self):
if self._full_value:
return self._full_value
key = self._full_value_cache_key.format(self.key)
cached = cache.get(key)
if cached:
return cached
if self.is_root():
return self.value
parent_full_value = self.parent.full_value
value = parent_full_value + ' / ' + self.value
self.full_value = value
if self._full_value is not None:
return self._full_value
print("Get full value")
value = self._tree.get_node_full_tag(self.key)
return value
@full_value.setter
def full_value(self, value):
self._full_value = value
key = self._full_value_cache_key.format(self.key)
cache.set(key, value, 3600*24)
def expire_full_value(self):
key = self._full_value_cache_key.format(self.key)
cache.delete_pattern(key+'*')
@classmethod
def expire_nodes_full_value(cls, nodes=None):
key = cls._full_value_cache_key.format('*')
cache.delete_pattern(key+'*')
class AssetsAmountMixin:
class NodeAssetsMixin:
_assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}'
_assets_cache_key = '_NODE_ASSETS_{}'
_assets_amount = None
key = ''
cache_time = 3600 * 24 * 7
id = None
@property
def assets_amount(self):
@ -207,40 +207,37 @@ class AssetsAmountMixin:
"""
if self._assets_amount is not None:
return self._assets_amount
cache_key = self._assets_amount_cache_key.format(self.key)
cached = cache.get(cache_key)
if cached is not None:
return cached
assets_amount = self.get_all_assets().count()
self.assets_amount = assets_amount
return assets_amount
amount = self._tree.assets_amount(self.key)
return amount
@assets_amount.setter
def assets_amount(self, value):
self._assets_amount = value
cache_key = self._assets_amount_cache_key.format(self.key)
cache.set(cache_key, value, self.cache_time)
# TOdo: 是否依赖tree
def get_all_assets(self):
from .asset import Asset
if self.is_root():
return Asset.objects.filter(org_id=self.org_id)
assets_ids = self._tree.all_assets(self.key)
return Asset.objects.filter(id__in=assets_ids)
def expire_assets_amount(self):
ancestor_keys = self.get_ancestor_keys(with_self=True)
cache_keys = [self._assets_amount_cache_key.format(k) for k in
ancestor_keys]
cache.delete_many(cache_keys)
def assets_ids(self):
assets_ids = self._tree.assets(self.key)
return assets_ids
@classmethod
def expire_nodes_assets_amount(cls, nodes=None):
key = cls._assets_amount_cache_key.format('*')
cache.delete_pattern(key)
def get_assets(self):
from .asset import Asset
if self.is_default_node():
assets = Asset.objects.filter(Q(nodes__id=self.id) | Q(nodes__isnull=True))
else:
assets = Asset.objects.filter(id=self.assets_ids())
return assets.distinct()
@classmethod
def refresh_nodes(cls):
from ..utils import NodeUtil
util = NodeUtil(with_assets_amount=True)
util.set_assets_amount()
util.set_full_value()
def get_valid_assets(self):
return self.get_assets().valid()
def get_all_valid_assets(self):
return self.get_all_assets().valid()
class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin):
class Node(OrgModelMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
value = models.CharField(max_length=128, verbose_name=_("Value"))
@ -256,7 +253,7 @@ class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin):
ordering = ['key']
def __str__(self):
return self.full_value
return self.value
def __eq__(self, other):
if not other:
@ -316,31 +313,9 @@ class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin):
child = self.__class__.objects.create(id=_id, key=child_key, value=value)
return child
def get_assets(self):
from .asset import Asset
if self.is_default_node():
assets = Asset.objects.filter(Q(nodes__id=self.id) | Q(nodes__isnull=True))
else:
assets = Asset.objects.filter(nodes__id=self.id)
return assets.distinct()
def get_valid_assets(self):
return self.get_assets().valid()
def get_all_assets(self):
from .asset import Asset
pattern = r'^{0}$|^{0}:'.format(self.key)
args = []
kwargs = {}
if self.is_root():
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
else:
kwargs['nodes__key__regex'] = pattern
assets = Asset.objects.filter(*args, **kwargs).distinct()
return assets
def get_all_valid_assets(self):
return self.get_all_assets().valid()
@classmethod
def refresh_nodes(cls):
cls.refresh_tree()
def is_default_node(self):
return self.is_root() and self.key == '1'
@ -410,19 +385,20 @@ class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin):
return
return super().delete(using=using, keep_parents=keep_parents)
@classmethod
def get_queryset(cls):
from ..utils import NodeUtil
util = NodeUtil()
return sorted(util.nodes)
@classmethod
def generate_fake(cls, count=100):
import random
org = get_current_org()
if not org or not org.is_real():
Organization.default().change_to()
i = 0
while i < count:
nodes = list(cls.objects.all())
if count > 100:
length = 100
else:
length = count
for i in range(count):
node = random.choice(cls.objects.all())
node.create_child('Node {}'.format(i))
for i in range(length):
node = random.choice(nodes)
node.create_child('Node {}'.format(i))

View File

@ -31,7 +31,7 @@ class AdminUser(AssetUser):
become = models.BooleanField(default=True)
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
become_user = models.CharField(default='root', max_length=64)
_become_pass = models.CharField(default='', max_length=128)
_become_pass = models.CharField(default='', blank=True, max_length=128)
CONNECTIVITY_CACHE_KEY = '_ADMIN_USER_CONNECTIVE_{}'
_prefer = "admin_user"

View File

@ -6,7 +6,7 @@ from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import Node, AdminUser
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializer, AuthSerializerMixin

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from django.db.models import Prefetch
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer
from ..models import Asset, Node, Label
from .base import ConnectivitySerializer

View File

@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AuthBook, Asset
from ..backends import AssetUserManager
from .base import ConnectivitySerializer, AuthSerializerMixin

View File

@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from common.fields import ChoiceDisplayField
from common.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule, SystemUser
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
class CommandFilterSerializer(BulkOrgResourceModelSerializer):

View File

@ -3,7 +3,7 @@
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Domain, Gateway
from .base import AuthSerializerMixin

View File

@ -3,7 +3,7 @@
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Label

View File

@ -2,7 +2,7 @@
from rest_framework import serializers
from django.utils.translation import ugettext as _
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node
@ -13,22 +13,21 @@ __all__ = [
class NodeSerializer(BulkOrgResourceModelSerializer):
assets_amount = serializers.IntegerField(read_only=True)
name = serializers.ReadOnlyField(source='value')
value = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("value"))
class Meta:
model = Node
only_fields = ['id', 'key', 'value', 'org_id']
fields = only_fields + ['name', 'assets_amount']
read_only_fields = [
'key', 'name', 'assets_amount', 'org_id',
]
fields = only_fields + ['name', 'full_value']
read_only_fields = ['key', 'org_id']
def validate_value(self, data):
instance = self.instance if self.instance else Node.root()
children = instance.parent.get_children()
children_values = [node.value for node in children if node != instance]
if data in children_values:
if not self.instance and not data:
return data
instance = self.instance
siblings = instance.get_siblings()
if siblings.filter(value=data):
raise serializers.ValidationError(
_('The same level node name cannot be the same')
)

View File

@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer
from common.utils import ssh_pubkey_gen
from orgs.mixins import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import SystemUser
from .base import AuthSerializer, AuthSerializerMixin

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models.signals import post_save, m2m_changed, post_delete
from django.db.models.signals import post_save, m2m_changed, pre_delete
from django.dispatch import receiver
from common.utils import get_logger
@ -38,15 +38,13 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
test_asset_conn_on_created(instance)
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
Node.refresh_nodes()
@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier")
@receiver(pre_delete, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_delete(sender, instance=None, **kwargs):
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
Node.refresh_nodes()
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
@ -80,19 +78,18 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
logger.debug("Asset nodes change signal received")
Asset.expire_all_nodes_keys_cache()
if isinstance(instance, Asset):
if kwargs['action'] == 'pre_remove':
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
# nodes = []
# if kwargs['action'] == 'pre_remove':
# nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
if kwargs['action'] == 'post_add':
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
system_users_assets = defaultdict(set)
system_users = SystemUser.objects.filter(nodes__in=nodes)
# 清理节点缓存
for system_user in system_users:
system_users_assets[system_user].update({instance})
for system_user, assets in system_users_assets.items():
system_user.assets.add(*tuple(assets))
Node.refresh_nodes()
@receiver(m2m_changed, sender=Asset.nodes.through)
@ -100,7 +97,6 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
if isinstance(instance, Node):
logger.debug("Node assets change signal {} received".format(instance))
# 当节点和资产关系发生改变时,过期资产数量缓存
instance.expire_assets_amount()
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
if kwargs['action'] == 'post_add':
# 重新关联系统用户和资产的关系
@ -112,7 +108,7 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
@receiver(post_save, sender=Node)
def on_node_update_or_created(sender, instance=None, created=False, **kwargs):
if instance and not created:
instance.expire_full_value()
Node.refresh_nodes()
@receiver(post_save, sender=AuthBook)

View File

@ -24,7 +24,7 @@ FORKS = 10
TIMEOUT = 60
logger = get_logger(__file__)
CACHE_MAX_TIME = 60*60*2
disk_pattern = re.compile(r'^hd|sd|xvd|vd')
disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "on")
@ -62,7 +62,7 @@ def clean_hosts_by_protocol(system_user, assets):
return hosts
@shared_task
@shared_task(queue="ansible")
def set_assets_hardware_info(assets, result, **kwargs):
"""
Using ops task run result, to update asset info
@ -106,7 +106,7 @@ def set_assets_hardware_info(assets, result, **kwargs):
for dev, dev_info in info.get('ansible_devices', {}).items():
if disk_pattern.match(dev) and dev_info['removable'] == '0':
disk_info[dev] = dev_info['size']
___disk_total = '%s %s' % sum_capacity(disk_info.values())
___disk_total = '%.1f %s' % sum_capacity(disk_info.values())
___disk_info = json.dumps(disk_info)
# ___platform = info.get('ansible_system', 'Unknown')
@ -148,7 +148,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
return result
@shared_task
@shared_task(queue="ansible")
def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname)
update_assets_hardware_info_util(
@ -156,7 +156,7 @@ def update_asset_hardware_info_manual(asset):
)
@shared_task
@shared_task(queue="ansible")
def update_assets_hardware_info_period():
"""
Update asset hardware period task
@ -170,7 +170,7 @@ def update_assets_hardware_info_period():
## ADMIN USER CONNECTIVE ##
@shared_task
@shared_task(queue="ansible")
def test_asset_connectivity_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
@ -227,7 +227,7 @@ def test_asset_connectivity_util(assets, task_name=None):
return results_summary
@shared_task
@shared_task(queue="ansible")
def test_asset_connectivity_manual(asset):
task_name = _("Test assets connectivity: {}").format(asset)
summary = test_asset_connectivity_util([asset], task_name=task_name)
@ -238,7 +238,7 @@ def test_asset_connectivity_manual(asset):
return True, ""
@shared_task
@shared_task(queue="ansible")
def test_admin_user_connectivity_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
@ -254,7 +254,7 @@ def test_admin_user_connectivity_util(admin_user, task_name):
return summary
@shared_task
@shared_task(queue="ansible")
@register_as_period_task(interval=3600)
def test_admin_user_connectivity_period():
"""
@ -276,7 +276,7 @@ def test_admin_user_connectivity_period():
cache.set(key, 1, 60*40)
@shared_task
@shared_task(queue="ansible")
def test_admin_user_connectivity_manual(admin_user):
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
@ -286,7 +286,7 @@ def test_admin_user_connectivity_manual(admin_user):
## System user connective ##
@shared_task
@shared_task(queue="ansible")
def test_system_user_connectivity_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
@ -344,14 +344,14 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
return results_summary
@shared_task
@shared_task(queue="ansible")
def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_all_assets()
return test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task
@shared_task(queue="ansible")
def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset
@ -359,7 +359,7 @@ def test_system_user_connectivity_a_asset(system_user, asset):
return test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task
@shared_task(queue="ansible")
def test_system_user_connectivity_period():
if PERIOD_TASK != "on":
logger.debug("Period task disabled, test system user connectivity pass")
@ -483,7 +483,7 @@ def get_push_system_user_tasks(host, system_user):
return tasks
@shared_task
@shared_task(queue="ansible")
def push_system_user_util(system_user, assets, task_name):
from ops.utils import update_or_create_ansible_task
if not system_user.is_need_push():
@ -519,14 +519,14 @@ def push_system_user_util(system_user, assets, task_name):
task.run()
@shared_task
@shared_task(queue="ansible")
def push_system_user_to_assets_manual(system_user):
assets = system_user.get_all_assets()
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name=task_name)
@shared_task
@shared_task(queue="ansible")
def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset
@ -534,7 +534,7 @@ def push_system_user_a_asset_manual(system_user, asset):
return push_system_user_util(system_user, [asset], task_name=task_name)
@shared_task
@shared_task(queue="ansible")
def push_system_user_to_assets(system_user, assets):
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name)
@ -569,7 +569,7 @@ def get_test_asset_user_connectivity_tasks(asset):
return tasks
@shared_task
@shared_task(queue="ansible")
def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False):
"""
:param asset_user: <AuthBook>对象
@ -602,7 +602,7 @@ def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False)
asset_user.set_connectivity(summary)
@shared_task
@shared_task(queue="ansible")
def test_asset_users_connectivity_manual(asset_users, run_as_admin=False):
"""
:param asset_users: <AuthBook>对象

View File

@ -236,7 +236,8 @@ function onBodyMouseDown(event){
}
function onRename(event, treeId, treeNode, isCancel){
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);
var data = {"value": treeNode.name};
if (isCancel){
return
@ -247,10 +248,13 @@ function onRename(event, treeId, treeNode, isCancel){
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')';
var assets_amount = treeNode.meta.node.assets_amount;
if (!assets_amount) {
assets_amount = 0;
}
treeNode.name = treeNode.name + ' (' + assets_amount + ')';
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
},
})
}

View File

@ -88,9 +88,9 @@
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Select nodes' %}" id="nodes_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
<select data-placeholder="{% trans 'Select nodes' %}" id="nodes_selected" class="nodes-select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.full_value }}</option>
{% endfor %}
</select>
</td>
@ -140,7 +140,8 @@ function replaceNodeAssetsAdminUser(nodes) {
jumpserver.nodes_selected = {};
$(document).ready(function () {
$('.select2').select2()
var url = "{% url 'api-assets:node-list' %}";
nodesSelect2Init(".nodes-select2", url)
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;

View File

@ -110,6 +110,8 @@ $(document).ready(function () {
$('.select2').select2({
allowClear: true
});
var url = "{% url 'api-assets:node-list' %}";
nodesSelect2Init(".nodes-select2", url);
$(".labels").select2({
allowClear: true,
templateSelection: format

View File

@ -195,10 +195,7 @@
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Nodes' %}" id="groups_selected" class="select2 groups" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
<select data-placeholder="{% trans 'Nodes' %}" id="groups_selected" class="nodes-select2 groups" style="width: 100%" multiple="" tabindex="4">
</select>
</td>
</tr>
@ -211,7 +208,7 @@
{% for node in asset.nodes.all %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.full_value }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-leave-node" type="button"><i class="fa fa-minus"></i></button>
</td>
@ -291,7 +288,9 @@ function refreshAssetHardware() {
$(document).ready(function () {
$('.select2.groups').select2().on('select2:select', function(evt) {
var url = "{% url 'api-assets:node-list' %}";
nodesSelect2Init(".nodes-select2", url)
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;
}).on('select2:unselect', function(evt) {

View File

@ -442,9 +442,10 @@ $(document).ready(function(){
var success = function () {
asset_table.ajax.reload()
};
var url = "{% url 'api-assets:node-remove-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
requestApi({
'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
'url': url,
'method': 'PUT',
'body': JSON.stringify(data),
'success': success

View File

@ -88,10 +88,7 @@
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="nodes-select2" style="width: 100%" multiple="" tabindex="4">
</select>
</td>
</tr>
@ -104,7 +101,7 @@
{% for node in system_user.nodes.all|sort %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.full_value }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
@ -156,6 +153,8 @@ jumpserver.nodes_selected = {};
$(document).ready(function () {
$('.select2').select2()
var url = "{% url 'api-assets:node-list' %}";
nodesSelect2Init(".nodes-select2", url)
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;

View File

@ -21,9 +21,10 @@
{% block custom_foot_js %}
<script>
var treeUrl = "{% url 'api-perms:my-nodes-as-tree' %}?&cache_policy=1";
var treeUrl = "{% url 'api-perms:my-nodes-children-as-tree' %}?&cache_policy=1";
var assetTableUrl = "{% url 'api-perms:my-assets' %}?cache_policy=1";
var selectUrl = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}?cache_policy=1&all=1';
var systemUsersUrl = "{% url 'api-perms:my-asset-system-users' asset_id=DEFAULT_PK %}";
var showAssetHref = false; // Need input default true
var actions = {
targets: 4, createdCell: function (td, cellData) {

View File

@ -1,33 +1,32 @@
# coding:utf-8
from django.urls import path
from django.urls import path, re_path
from rest_framework_nested import routers
# from rest_framework.routers import DefaultRouter
from rest_framework_bulk.routes import BulkRouter
from common import api as capi
from .. import api
app_name = 'assets'
router = BulkRouter()
router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'admin-user', api.AdminUserViewSet, 'admin-user')
router.register(r'system-user', api.SystemUserViewSet, 'system-user')
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
router.register(r'system-users', api.SystemUserViewSet, 'system-user')
router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node')
router.register(r'domain', api.DomainViewSet, 'domain')
router.register(r'gateway', api.GatewayViewSet, 'gateway')
router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter')
router.register(r'asset-user', api.AssetUserViewSet, 'asset-user')
router.register(r'asset-user-info', api.AssetUserExportViewSet, 'asset-user-info')
router.register(r'domains', api.DomainViewSet, 'domain')
router.register(r'gateways', api.GatewayViewSet, 'gateway')
router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter')
router.register(r'asset-users', api.AssetUserViewSet, 'asset-user')
router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('asset/update/select/',
api.AssetBulkUpdateSelectAPI.as_view(), name='asset-bulk-update-select'),
path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/',
@ -35,36 +34,36 @@ urlpatterns = [
path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('asset-user/auth-info/',
path('asset-users/auth-info/',
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),
path('asset-user/test-connective/',
path('asset-users/test-connective/',
api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'),
path('admin-user/<uuid:pk>/nodes/',
path('admin-users/<uuid:pk>/nodes/',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-user/<uuid:pk>/auth/',
path('admin-users/<uuid:pk>/auth/',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/',
path('admin-users/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-user/<uuid:pk>/assets/',
path('admin-users/<uuid:pk>/assets/',
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-user/<uuid:pk>/auth-info/',
path('system-users/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/auth-info/',
path('system-users/<uuid:pk>/asset/<uuid:aid>/auth-info/',
api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-user/<uuid:pk>/assets/',
path('system-users/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-user/<uuid:pk>/push/',
path('system-users/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
path('system-users/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
path('system-users/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/',
path('system-users/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('system-user/<uuid:pk>/cmd-filter-rules/',
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'),
@ -89,10 +88,14 @@ urlpatterns = [
path('nodes/refresh-assets-amount/',
api.RefreshAssetsAmount.as_view(), name='refresh-assets-amount'),
path('gateway/<uuid:pk>/test-connective/',
path('gateways/<uuid:pk>/test-connective/',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
]
urlpatterns += router.urls + cmd_filter_router.urls
old_version_urlpatterns = [
re_path('(?P<resource>admin-user|system-user|domain|gateway|cmd-filter|asset-user)/.*', capi.redirect_plural_name_api)
]
urlpatterns += router.urls + cmd_filter_router.urls + old_version_urlpatterns

View File

@ -1,12 +1,14 @@
# ~*~ coding: utf-8 ~*~
#
import time
from functools import reduce
from django.db.models import Prefetch, Q
from treelib import Tree
from collections import defaultdict
from copy import deepcopy
import threading
from django.db.models import Q
from common.utils import get_object_or_none, get_logger
from common.struct import Stack
from .models import SystemUser, Label, Node, Asset
from common.utils import get_object_or_none, get_logger, timeit
from .models import SystemUser, Label, Asset
logger = get_logger(__file__)
@ -53,204 +55,141 @@ class LabelFilter(LabelFilterMixin):
return queryset
class NodeUtil:
def __init__(self, with_assets_amount=False, debug=False):
self.stack = Stack()
self._nodes = {}
self.with_assets_amount = with_assets_amount
self._debug = debug
self.init()
class TreeService(Tree):
tag_sep = ' / '
cache_key = '_NODE_FULL_TREE'
cache_time = 3600
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.nodes_assets_map = defaultdict(set)
self.all_nodes_assets_map = {}
self.mutex = threading.Lock()
@classmethod
@timeit
def new(cls):
from .models import Node
from orgs.utils import get_current_org, set_to_root_org
origin_org = get_current_org()
set_to_root_org()
all_nodes = Node.objects.all()
origin_org.change_to()
tree = cls()
tree.create_node(tag='', identifier='')
for node in all_nodes:
tree.create_node(
tag=node.value, identifier=node.key,
parent=node.parent_key,
)
tree.init_assets_async()
return tree
def init_assets_async(self):
t = threading.Thread(target=self.init_assets)
t.start()
def init_assets(self):
from orgs.utils import get_current_org, set_to_root_org
with self.mutex:
origin_org = get_current_org()
set_to_root_org()
queryset = Asset.objects.all().valid().values_list('id', 'nodes__key')
if origin_org:
origin_org.change_to()
for asset_id, key in queryset:
if not key:
continue
self.nodes_assets_map[key].add(asset_id)
def all_children(self, nid, with_self=True, deep=False):
children_ids = self.expand_tree(nid)
if not with_self:
next(children_ids)
return [self.get_node(i, deep=deep) for i in children_ids]
def ancestors(self, nid, with_self=False, deep=False):
ancestor_ids = list(self.rsearch(nid))
ancestor_ids.pop()
if not with_self:
ancestor_ids.pop(0)
return [self.get_node(i, deep=deep) for i in ancestor_ids]
def get_node_full_tag(self, nid):
ancestors = self.ancestors(nid)
ancestors.reverse()
return self.tag_sep.join(n.tag for n in ancestors)
def get_family(self, nid, deep=False):
ancestors = self.ancestors(nid, with_self=False, deep=deep)
children = self.all_children(nid, with_self=False)
return ancestors + [self[nid]] + children
def root_node(self):
return self.get_node(self.root)
def get_node(self, nid, deep=False):
node = super().get_node(nid)
if deep:
node = self.copy_node(node)
return node
def parent(self, nid, deep=False):
parent = super().parent(nid)
if deep:
parent = self.copy_node(parent)
return parent
def assets(self, nid):
with self.mutex:
assets = self.nodes_assets_map[nid]
return assets
def set_assets(self, nid, assets):
with self.mutex:
self.nodes_assets_map[nid] = assets
def all_assets(self, nid):
assets = self.all_nodes_assets_map.get(nid)
if assets:
return assets
assets = set(self.assets(nid))
children = self.children(nid)
for child in children:
assets.update(self.all_assets(child.identifier))
return assets
def assets_amount(self, nid):
return len(self.all_assets(nid))
@staticmethod
def sorted_by(node):
return [int(i) for i in node.key.split(':')]
def copy_node(node):
new_node = deepcopy(node)
new_node.fpointer = None
return new_node
def get_queryset(self):
all_nodes = Node.objects.all()
if self.with_assets_amount:
all_nodes = all_nodes.prefetch_related(
Prefetch('assets', queryset=Asset.objects.all().only('id'))
)
all_nodes = list(all_nodes)
for node in all_nodes:
node._assets = set(node.assets.all())
return all_nodes
def get_all_nodes(self):
all_nodes = sorted(self.get_queryset(), key=self.sorted_by)
guarder = Node(key='', value='Guarder')
guarder._assets = []
all_nodes.append(guarder)
return all_nodes
def push_to_stack(self, node):
# 入栈之前检查
# 如果栈是空的,证明是一颗树的根部
if self.stack.is_empty():
node._full_value = node.value
node._parents = []
def safe_add_ancestors(self, ancestors):
# 如果祖先节点为1个那么添加该节点, 父节点是root node
if len(ancestors) == 1:
node = ancestors[0]
parent = self.root_node()
else:
# 如果不是根节点,
# 该节点的祖先应该是父节点的祖先加上父节点
# 该节点的名字是父节点的名字+自己的名字
node._parents = [self.stack.top] + self.stack.top._parents
node._full_value = ' / '.join(
[self.stack.top._full_value, node.value]
)
node._children = []
node._all_children = []
self.debug("入栈: {}".format(node.key))
self.stack.push(node)
# 出栈
def pop_from_stack(self):
_node = self.stack.pop()
self.debug("出栈: {} 栈顶: {}".format(_node.key, self.stack.top.key if self.stack.top else None))
self._nodes[_node.key] = _node
if not self.stack.top:
return
if self.with_assets_amount:
self.stack.top._assets.update(_node._assets)
_node._assets_amount = len(_node._assets)
delattr(_node, '_assets')
self.stack.top._children.append(_node)
self.stack.top._all_children.extend([_node] + _node._all_children)
def init(self):
all_nodes = self.get_all_nodes()
for node in all_nodes:
self.debug("准备: {} 栈顶: {}".format(node.key, self.stack.top.key if self.stack.top else None))
# 入栈之前检查,该节点是不是栈顶节点的子节点
# 如果不是,则栈顶出栈
while self.stack.top and not self.stack.top.is_children(node):
self.pop_from_stack()
self.push_to_stack(node)
# 出栈最后一个
self.debug("剩余: {}".format(', '.join([n.key for n in self.stack])))
def get_nodes_by_queryset(self, queryset):
nodes = []
for n in queryset:
node = self.get_node_by_key(n.key)
if not node:
continue
nodes.append(node)
return nodes
def get_node_by_key(self, key):
return self._nodes.get(key)
def debug(self, msg):
self._debug and logger.debug(msg)
def set_assets_amount(self):
for node in self._nodes.values():
node.assets_amount = node._assets_amount
def set_full_value(self):
for node in self._nodes.values():
node.full_value = node._full_value
@property
def nodes(self):
return list(self._nodes.values())
def get_family_by_key(self, key):
tree_nodes = set()
node = self.get_node_by_key(key)
if not node:
return []
tree_nodes.update(node._parents)
tree_nodes.add(node)
tree_nodes.update(node._all_children)
return list(tree_nodes)
# 使用给定节点生成一颗树
# 找到他们的祖先节点
# 可选找到他们的子孙节点
def get_family(self, node):
return self.get_family_by_key(node.key)
def get_family_keys_by_key(self, key):
nodes = self.get_family_by_key(key)
return [n.key for n in nodes]
def get_some_nodes_family_by_keys(self, keys):
family = set()
for key in keys:
family.update(self.get_family_by_key(key))
return family
def get_some_nodes_family_keys_by_keys(self, keys):
family = self.get_some_nodes_family_by_keys(keys)
return [n.key for n in family]
def get_nodes_parents_by_key(self, key, with_self=True):
parents = set()
node = self.get_node_by_key(key)
if not node:
return []
parents.update(set(node._parents))
if with_self:
parents.add(node)
return list(parents)
def get_node_parents(self, node, with_self=True):
return self.get_nodes_parents_by_key(node.key, with_self=with_self)
def get_nodes_parents_keys_by_key(self, key, with_self=True):
nodes = self.get_nodes_parents_by_key(key, with_self=with_self)
return [n.key for n in nodes]
def get_all_children_by_key(self, key, with_self=True):
children = set()
node = self.get_node_by_key(key)
if not node:
return []
children.update(set(node._all_children))
if with_self:
children.add(node)
return list(children)
def get_all_children(self, node, with_self=True):
return self.get_all_children_by_key(node.key, with_self=with_self)
def get_all_children_keys_by_key(self, key, with_self=True):
nodes = self.get_all_children_by_key(key, with_self=with_self)
return [n.key for n in nodes]
def test_node_tree():
tree = NodeUtil()
for node in tree._nodes.values():
print("Check {}".format(node.key))
children_wanted = node.get_all_children().count()
children = len(node._children)
if children != children_wanted:
print("{} children not equal: {} != {}".format(node.key, children, children_wanted))
assets_amount_wanted = node.get_all_assets().count()
if node._assets_amount != assets_amount_wanted:
print("{} assets amount not equal: {} != {}".format(
node.key, node._assets_amount, assets_amount_wanted)
)
full_value_wanted = node.full_value
if node._full_value != full_value_wanted:
print("{} full value not equal: {} != {}".format(
node.key, node._full_value, full_value_wanted)
)
parents_wanted = node.get_ancestor().count()
parents = len(node._parents)
if parents != parents_wanted:
print("{} parents count not equal: {} != {}".format(
node.key, parents, parents_wanted)
)
node, ancestors = ancestors[0], ancestors[1:]
parent_id = ancestors[0].identifier
# 如果父节点不存在, 则先添加父节点
if not self.contains(parent_id):
self.safe_add_ancestors(ancestors)
parent = self.get_node(parent_id)
print("Add node: {} {}".format(node.identifier, parent.identifier))
# 如果当前节点已再树中,则移动当前节点到父节点中
# 这个是由于 当前节点放到了二级节点中
if self.contains(node.identifier):
self.move_node(node.identifier, parent.identifier)
else:
self.add_node(node, parent)

View File

@ -83,7 +83,6 @@ class AdminUserDetailView(PermissionsMixin, DetailView):
context = {
'app': _('Assets'),
'action': _('Admin user detail'),
'nodes': Node.get_queryset(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -16,8 +16,7 @@ from common.utils import get_object_or_none, get_logger
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
from common.const import KEY_CACHE_RESOURCES_ID
from .. import forms
from ..utils import NodeUtil
from ..models import Asset, SystemUser, Label, Node
from ..models import Asset, Label, Node
__all__ = [
@ -196,13 +195,9 @@ class AssetDetailView(PermissionsMixin, DetailView):
).select_related('admin_user', 'domain')
def get_context_data(self, **kwargs):
nodes_remain = Node.objects.exclude(assets=self.object).only('key')
util = NodeUtil()
nodes_remain = util.get_nodes_by_queryset(nodes_remain)
context = {
'app': _('Assets'),
'action': _('Asset detail'),
'nodes_remain': nodes_remain,
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -98,14 +98,9 @@ class SystemUserAssetView(PermissionsMixin, DetailView):
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
from ..utils import NodeUtil
nodes_remain = Node.objects.exclude(systemuser=self.object)
util = NodeUtil()
nodes_remain = util.get_nodes_by_queryset(nodes_remain)
context = {
'app': _('assets'),
'action': _('System user asset'),
'nodes_remain': nodes_remain
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -1,4 +1,6 @@
from django.apps import AppConfig
from django.conf import settings
from django.db.models.signals import post_save
class AuditsConfig(AppConfig):
@ -6,3 +8,5 @@ class AuditsConfig(AppConfig):
def ready(self):
from . import signals_handler
if settings.SYSLOG_ENABLE:
post_save.connect(signals_handler.on_audits_log_create)

View File

@ -0,0 +1,35 @@
# Generated by Django 2.1.7 on 2019-07-26 09:53
from django.db import migrations, models
def migrate_loginlog_reason_to_str(apps, schema_editor):
db_alias = schema_editor.connection.alias
reason_map = {
"0": "",
"1": 'Username/password check failed',
"2": 'MFA authentication failed',
"3": "Username does not exist",
"4": "Password expired",
}
model = apps.get_model("audits", "UserLoginLog")
for k, v in reason_map.items():
model.objects.using(db_alias).filter(reason=k).update(reason=v)
class Migration(migrations.Migration):
dependencies = [
('audits', '0005_auto_20190228_1715'),
]
operations = [
migrations.AlterField(
model_name='userloginlog',
name='reason',
field=models.CharField(blank=True, default='', max_length=128, verbose_name='Reason'),
),
migrations.RunPython(migrate_loginlog_reason_to_str),
]

View File

@ -5,7 +5,7 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from orgs.mixins import OrgModelMixin
from orgs.mixins.models import OrgModelMixin
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
@ -72,20 +72,6 @@ class UserLoginLog(models.Model):
(MFA_UNKNOWN, _('-')),
)
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
@ -97,7 +83,7 @@ class UserLoginLog(models.Model):
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
reason = models.SmallIntegerField(default=0, choices=REASON_CHOICE, verbose_name=_('Reason'))
reason = models.CharField(default='', max_length=128, blank=True, verbose_name=_('Reason'))
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))

View File

@ -3,11 +3,36 @@
from rest_framework import serializers
from .models import FTPLog
from terminal.models import Session
from . import models
class FTPLogSerializer(serializers.ModelSerializer):
class Meta:
model = FTPLog
model = models.FTPLog
fields = '__all__'
class LoginLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserLoginLog
fields = '__all__'
class OperateLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.OperateLog
fields = '__all__'
class PasswordChangeLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.PasswordChangeLog
fields = '__all__'
class SessionAuditSerializer(serializers.ModelSerializer):
class Meta:
model = Session
fields = '__all__'

View File

@ -4,13 +4,18 @@
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db import transaction
from rest_framework.renderers import JSONRenderer
from jumpserver.utils import current_request
from common.utils import get_request_ip, get_logger
from common.utils import get_request_ip, get_logger, get_syslogger
from users.models import User
from .models import OperateLog, PasswordChangeLog
from terminal.models import Session
from . import models
from . import serializers
logger = get_logger(__name__)
sys_logger = get_syslogger("audits")
json_render = JSONRenderer()
MODELS_NEED_RECORD = (
@ -36,7 +41,7 @@ def create_operate_log(action, sender, resource):
}
with transaction.atomic():
try:
OperateLog.objects.create(**data)
models.OperateLog.objects.create(**data)
except Exception as e:
logger.error("Create operate log error: {}".format(e))
@ -44,15 +49,15 @@ def create_operate_log(action, sender, resource):
@receiver(post_save, dispatch_uid="my_unique_identifier")
def on_object_created_or_update(sender, instance=None, created=False, **kwargs):
if created:
action = OperateLog.ACTION_CREATE
action = models.OperateLog.ACTION_CREATE
else:
action = OperateLog.ACTION_UPDATE
action = models.OperateLog.ACTION_UPDATE
create_operate_log(action, sender, instance)
@receiver(post_delete, dispatch_uid="my_unique_identifier")
def on_object_delete(sender, instance=None, **kwargs):
create_operate_log(OperateLog.ACTION_DELETE, sender, instance)
create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance)
@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier")
@ -61,7 +66,32 @@ def on_user_change_password(sender, instance=None, **kwargs):
if not current_request or not current_request.user.is_authenticated:
return
with transaction.atomic():
PasswordChangeLog.objects.create(
models.PasswordChangeLog.objects.create(
user=instance, change_by=current_request.user,
remote_addr=get_request_ip(current_request),
)
def on_audits_log_create(sender, instance=None, **kwargs):
if sender == models.UserLoginLog:
category = "login_log"
serializer = serializers.LoginLogSerializer
elif sender == models.FTPLog:
serializer = serializers.FTPLogSerializer
category = "ftp_log"
elif sender == models.OperateLog:
category = "operation_log"
serializer = serializers.OperateLogSerializer
elif sender == models.PasswordChangeLog:
category = "password_change_log"
serializer = serializers.PasswordChangeLogSerializer
elif sender == Session:
category = "host_session_log"
serializer = serializers.SessionAuditSerializer
else:
return
s = serializer(instance=instance)
data = json_render.render(s.data).decode(errors='ignore')
msg = "{} - {}".format(category, data)
sys_logger.info(msg)

View File

@ -72,7 +72,7 @@
<td class="text-center">{{ login_log.ip }}</td>
<td class="text-center">{{ login_log.city }}</td>
<td class="text-center">{{ login_log.get_mfa_display }}</td>
<td class="text-center">{{ login_log.get_reason_display }}</td>
<td class="text-center">{% trans login_log.reason %}</td>
<td class="text-center">{{ login_log.get_status_display }}</td>
<td class="text-center">{{ login_log.datetime }}</td>
</tr>

View File

@ -2,3 +2,6 @@
#
from .auth import *
from .token import *
from .mfa import *
from .access_key import *

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
#
from rest_framework.viewsets import ModelViewSet
from common.permissions import IsValidUser
from .. import serializers
class AccessKeyViewSet(ModelViewSet):
permission_classes = (IsValidUser,)
serializer_class = serializers.AccessKeySerializer
search_fields = ['^id', '^secret']
def get_queryset(self):
return self.request.user.access_keys.all()
def perform_create(self, serializer):
user = self.request.user
user.create_access_key()

View File

@ -16,15 +16,17 @@ from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
from orgs.mixins import RootOrgViewMixin
from orgs.mixins.api import RootOrgViewMixin
from users.serializers import UserSerializer
from users.models import User
from assets.models import Asset, SystemUser
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_user_valid, check_otp_code, increase_login_failed_count,
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from .. import const
from ..utils import check_user_valid
from ..serializers import OtpVerifySerializer
from ..signals import post_auth_success, post_auth_failed
@ -53,27 +55,15 @@ class UserAuthApi(RootOrgViewMixin, APIView):
user, msg = self.check_user_valid(request)
if not user:
username = request.data.get('username', '')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
self.send_auth_signal(success=False, username=username, reason=reason)
self.send_auth_signal(success=False, username=username, reason=msg)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if user.password_has_expired:
self.send_auth_signal(
success=False, username=username,
reason=LoginLog.REASON_PASSWORD_EXPIRED
)
msg = _("The user {} password has expired, please update.".format(
user.username))
logger.info(msg)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
self.send_auth_signal(success=True, user=user)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token = user.create_bearer_token(request)
token, expired_at = user.create_bearer_token(request)
return Response(
{'token': token, 'user': self.serializer_class(user).data}
)
@ -167,10 +157,10 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA)
self.send_auth_signal(success=False, username=user.username, reason=const.mfa_failed)
return Response({'msg': _('MFA certification failed')}, status=401)
self.send_auth_signal(success=True, user=user)
token = user.create_bearer_token(request)
token, expired_at = user.create_bearer_token(request)
data = {'token': token, 'user': self.serializer_class(user).data}
return Response(data)

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView
from .. import serializers
class MFAChallengeApi(CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.MFAChallengeSerializer

View File

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from drf_yasg.utils import swagger_auto_schema
from common.utils import get_request_ip, get_logger
from users.utils import (
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from ..utils import check_user_valid
from ..signals import post_auth_success, post_auth_failed
from .. import serializers
logger = get_logger(__name__)
__all__ = ['TokenCreateApi']
class AuthFailedError(Exception):
def __init__(self, msg, reason=None):
self.msg = msg
self.reason = reason
class MFARequiredError(Exception):
pass
class TokenCreateApi(CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.BearerTokenSerializer
@staticmethod
def check_is_block(username, ip):
if is_block_login(username, ip):
msg = _("Log in frequently and try again later")
logger.warn(msg + ': ' + username + ':' + ip)
raise AuthFailedError(msg)
def check_user_valid(self):
request = self.request
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, password=password,
public_key=public_key
)
if not user:
raise AuthFailedError(msg)
return user
def create(self, request, *args, **kwargs):
username = self.request.data.get('username')
ip = self.request.data.get('remote_addr', None)
ip = ip or get_request_ip(self.request)
user = None
try:
self.check_is_block(username, ip)
user = self.check_user_valid()
if user.otp_enabled:
raise MFARequiredError()
self.send_auth_signal(success=True, user=user)
clean_failed_count(username, ip)
return super().create(request, *args, **kwargs)
except AuthFailedError as e:
increase_login_failed_count(username, ip)
self.send_auth_signal(success=False, user=user, username=username, reason=str(e))
return Response({'msg': str(e)}, status=401)
except MFARequiredError:
msg = _("MFA required")
seed = uuid.uuid4().hex
cache.set(seed, user.username, 300)
resp = {'msg': msg, "choices": ["otp"], "req": seed}
return Response(resp, status=300)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(
sender=self.__class__, user=user, request=self.request
)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)

View File

@ -11,6 +11,7 @@ from django.utils.six import text_type
from django.contrib.auth import get_user_model
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions
from common.auth import signature
from rest_framework.authentication import CSRFCheck
from common.utils import get_object_or_none, make_signature, http_to_unixtime
@ -108,8 +109,8 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
class AccessTokenAuthentication(authentication.BaseAuthentication):
keyword = 'Bearer'
model = get_user_model()
expiration = settings.TOKEN_EXPIRATION or 3600
model = get_user_model()
def authenticate(self, request):
auth = authentication.get_authorization_header(request).split()
@ -133,8 +134,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
return self.authenticate_credentials(token)
def authenticate_credentials(self, token):
model = get_user_model()
user_id = cache.get(token)
user = get_object_or_none(self.model, id=user_id)
user = get_object_or_none(model, id=user_id)
if not user:
msg = _('Invalid token or cache refreshed.')
@ -167,3 +169,25 @@ class SessionAuthentication(authentication.SessionAuthentication):
# CSRF passed with authenticated user
return user, None
class SignatureAuthentication(signature.SignatureAuthentication):
# The HTTP header used to pass the consumer key ID.
# A method to fetch (User instance, user_secret_string) from the
# consumer key ID, or None in case it is not found. Algorithm
# will be what the client has sent, in the case that both RSA
# and HMAC are supported at your site (and also for expansion).
model = get_user_model()
def fetch_user_data(self, key_id, algorithm="hmac-sha256"):
# ...
# example implementation:
try:
key = AccessKey.objects.get(id=key_id)
if not key.is_active:
return None, None
user, secret = key.user, str(key.secret)
return user, secret
except AccessKey.DoesNotExist:
return None, None

View File

@ -1,4 +1,10 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
password_failed = _('Username/password check failed')
mfa_failed = _('MFA authentication failed')
user_not_exist = _("Username does not exist")
password_expired = _("Password expired")
user_invalid = _('Disabled or expired')

View File

@ -0,0 +1,26 @@
# Generated by Django 2.1.7 on 2019-07-29 06:23
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('authentication', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='accesskey',
name='date_created',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2019, 7, 29, 6, 23, 54, 115123, tzinfo=utc)),
preserve_default=False,
),
migrations.AddField(
model_name='accesskey',
name='is_active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
]

View File

@ -12,6 +12,8 @@ class AccessKey(models.Model):
default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
on_delete=models.CASCADE, related_name='access_keys')
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
date_created = models.DateTimeField(auto_now_add=True)
def get_id(self):
return str(self.id)

View File

@ -1,20 +1,89 @@
# -*- coding: utf-8 -*-
#
from django.core.cache import cache
from rest_framework import serializers
from users.models import User
from .models import AccessKey
__all__ = ['AccessKeySerializer']
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer',
]
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret']
read_only_fields = ['id', 'secret']
fields = ['id', 'secret', 'is_active', 'date_created']
read_only_fields = ['id', 'secret', 'date_created']
class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6)
class BearerTokenMixin(serializers.Serializer):
token = serializers.CharField(read_only=True)
keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True)
@staticmethod
def get_keyword(obj):
return 'Bearer'
def create_response(self, username):
request = self.context.get("request")
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise serializers.ValidationError("username %s not exist" % username)
token, date_expired = user.create_bearer_token(request)
instance = {
"username": username,
"token": token,
"date_expired": date_expired,
}
return instance
def update(self, instance, validated_data):
pass
class BearerTokenSerializer(BearerTokenMixin, serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True, allow_null=True,
required=False)
public_key = serializers.CharField(write_only=True, allow_null=True,
required=False)
def create(self, validated_data):
username = validated_data.get("username")
return self.create_response(username)
class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer):
req = serializers.CharField(write_only=True)
auth_type = serializers.CharField(write_only=True)
code = serializers.CharField(write_only=True)
def validate_req(self, attr):
username = cache.get(attr)
if not username:
raise serializers.ValidationError("Not valid, may be expired")
self.context["username"] = username
def validate_code(self, code):
username = self.context["username"]
user = User.objects.get(username=username)
ok = user.check_otp(code)
if not ok:
msg = "Otp code not valid, may be expired"
raise serializers.ValidationError(msg)
def create(self, validated_data):
username = self.context["username"]
return self.create_response(username)

View File

@ -0,0 +1,144 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_id %}access_key_modal{% endblock %}
{% block modal_class %}modal-lg{% endblock %}
{% block modal_title%}{% trans "API key list" %}{% endblock %}
{% block modal_body %}
<style>
.inmodal .modal-body {
background: #fff;
}
#access_key_list_table_wrapper {
padding-top: 10px;
}
</style>
<div class="alert alert-info help-message">
{% trans 'Using api key sign api header, every requests header difference'%}, <a href="https://tools.ietf.org/html/draft-cavage-http-signatures-08">{% trans 'docs' %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="access_key_list_table" style="padding-top: 10px">
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'ID' %}</th>
<th class="text-center">{% trans 'Secret' %}</th>
<th class="text-center">{% trans 'Active' %}</th>
<th class="text-center">{% trans 'Date' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="uc" hidden>
<button class="btn btn-primary btn-sm" id="create-btn" href="#"> {% trans "Create" %} </button>
</div>
<script>
var table = null;
function initTable() {
var options = {
ele: $('#access_key_list_table'),
columnDefs: [
{targets: 2, createdCell: function (td, cellData) {
var btn = '<button class="btn btn-primary btn-xs btn-secret" data-secret="SECRET">{% trans 'Show' %}</button>';
btn = btn.replace("SECRET", cellData);
$(td).html(btn)
}},
{targets: 3, createdCell: function (td, cellData) {
if (cellData) {
$(td).html('<i class="fa fa-check text-navy"></i>')
} else {
$(td).html('<i class="fa fa-times text-danger"></i>')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var date = toSafeLocalDateStr(cellData);
$(td).html(date)
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var btn = '';
var btn_del = '<a class="btn btn-xs btn-danger m-l-xs btn-del" data-id="ID">{% trans "Delete" %}</a>';
var btn_inactive = '<a class="btn btn-xs btn-info m-l-xs btn-inactive" data-id="ID">{% trans "Disable" %}</a>';
var btn_active = '<a class="btn btn-xs btn-primary m-l-xs btn-active" data-id="ID">{% trans "Enable" %}</a>';
btn += btn_del;
if (rowData.is_active) {
btn += btn_inactive
} else {
btn += btn_active
}
btn = btn.replaceAll("ID", cellData);
$(td).html(btn);
}}
],
ajax_url: '{% url "api-auth:access-key-list" %}',
columns: [
{data: "id"},
{data: "id"},
{data: "secret"},
{data: "is_active"},
{data: "date_created"},
{data: "id", orderable: false}
],
uc_html: $('#uc').html()
};
table = jumpserver.initServerSideDataTable(options);
}
$(document).ready(function () {
}).on("show.bs.modal", "#access_key_modal", function () {
if (!table) {
initTable()
}
}).on("click", "#create-btn", function () {
var url = "{% url "api-auth:access-key-list" %}";
var data = {
url: url,
method: 'POST',
success: function () {
table.ajax.reload();
}
};
requestApi(data)
}).on("click", ".btn-secret", function () {
var $this = $(this);
$this.parent().html($this.data("secret"))
}).on("click", ".btn-del", function () {
var url = "{% url "api-auth:access-key-detail" pk=DEFAULT_PK %}";
url = url.replace("{{ DEFAULT_PK }}", $(this).data("id")) ;
objectDelete($(this), $(this).data("id"), url);
}).on("click", ".btn-active", function () {
var url = "{% url "api-auth:access-key-detail" pk=DEFAULT_PK %}";
url = url.replace("{{ DEFAULT_PK }}", $(this).data("id")) ;
var data = {
url: url,
body: JSON.stringify({"is_active": true}),
method: "PATCH",
success: function () {
table.ajax.reload();
}
};
requestApi(data)
}).on("click", ".btn-inactive", function () {
var url = "{% url "api-auth:access-key-detail" pk=DEFAULT_PK %}";
url = url.replace("{{ DEFAULT_PK }}", $(this).data("id")) ;
var data = {
url: url,
body: JSON.stringify({"is_active": false}),
method: "PATCH",
success: function () {
table.ajax.reload();
}
};
requestApi(data)
})
</script>
{% endblock %}
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
{% endblock %}

View File

@ -18,7 +18,7 @@
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
<!-- scripts -->
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>

View File

@ -4,18 +4,27 @@
from __future__ import absolute_import
from django.urls import path
from rest_framework.routers import DefaultRouter
from .. import api
router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
app_name = 'authentication'
urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
]
urlpatterns += router.urls

View File

@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from common.utils import get_ip_city, validate_ip
from django.contrib.auth import authenticate
from common.utils import get_ip_city, get_object_or_none, validate_ip
from users.models import User
from . import const
def write_login_log(*args, **kwargs):
@ -16,3 +20,36 @@ def write_login_log(*args, **kwargs):
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
def check_user_valid(**kwargs):
password = kwargs.pop('password', None)
public_key = kwargs.pop('public_key', None)
email = kwargs.pop('email', None)
username = kwargs.pop('username', None)
if username:
user = get_object_or_none(User, username=username)
elif email:
user = get_object_or_none(User, email=email)
else:
user = None
if user is None:
return None, const.user_not_exist
elif not user.is_valid:
return None, const.user_invalid
elif user.password_has_expired:
return None, const.password_expired
if password and authenticate(username=username, password=password):
return user, ''
if public_key and user.public_key:
public_key_saved = user.public_key.split()
if len(public_key_saved) == 1:
if public_key == public_key_saved[0]:
return user, ''
elif len(public_key_saved) > 1:
if public_key == public_key_saved[1]:
return user, ''
return None, const.password_failed

View File

@ -26,6 +26,7 @@ from users.utils import (
)
from ..signals import post_auth_success, post_auth_failed
from .. import forms
from .. import const
__all__ = [
@ -81,7 +82,7 @@ class UserLoginView(FormView):
user = form.get_user()
# user password expired
if user.password_has_expired:
reason = LoginLog.REASON_PASSWORD_EXPIRED
reason = const.password_expired
self.send_auth_signal(success=False, username=user.username, reason=reason)
return self.render_to_response(self.get_context_data(password_expired=True))
@ -96,7 +97,7 @@ class UserLoginView(FormView):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
reason = const.password_failed if exist else const.user_not_exist
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
@ -167,7 +168,7 @@ class UserLoginOtpView(FormView):
else:
self.send_auth_signal(
success=False, username=user.username,
reason=LoginLog.REASON_MFA
reason=const.mfa_failed
)
form.add_error(
'otp_code', _('MFA code invalid, or ntp sync server time')

View File

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

View File

@ -0,0 +1,112 @@
from rest_framework import authentication
from rest_framework import exceptions
from httpsig import HeaderVerifier, utils
"""
Reusing failure exceptions serves several purposes:
1. Lack of useful information regarding the failure inhibits attackers
from learning about valid keyIDs or other forms of information leakage.
Using the same actual object for any failure makes preventing such
leakage through mistakenly-distinct error messages less likely.
2. In an API scenario, the object is created once and raised many times
rather than generated on every failure, which could lead to higher loads
or memory usage in high-volume attack scenarios.
"""
FAILED = exceptions.AuthenticationFailed('Invalid signature.')
class SignatureAuthentication(authentication.BaseAuthentication):
"""
DRF authentication class for HTTP Signature support.
You must subclass this class in your own project and implement the
`fetch_user_data(self, keyId, algorithm)` method, returning a tuple of
the User object and a bytes object containing the user's secret. Note
that key_id and algorithm are DIRTY as they are supplied by the client
and so must be verified in your subclass!
You may set the following class properties in your subclass to configure
authentication for your particular use case:
:param www_authenticate_realm: Default: "api"
:param required_headers: Default: ["(request-target)", "date"]
"""
www_authenticate_realm = "api"
required_headers = ["(request-target)", "date"]
def fetch_user_data(self, key_id, algorithm=None):
"""Retuns a tuple (User, secret) or (None, None)."""
raise NotImplementedError()
def authenticate_header(self, request):
"""
DRF sends this for unauthenticated responses if we're the primary
authenticator.
"""
h = " ".join(self.required_headers)
return 'Signature realm="%s",headers="%s"' % (
self.www_authenticate_realm, h)
def authenticate(self, request):
"""
Perform the actual authentication.
Note that the exception raised is always the same. This is so that we
don't leak information about in/valid keyIds and other such useful
things.
"""
auth_header = authentication.get_authorization_header(request)
if not auth_header or len(auth_header) == 0:
return None
method, fields = utils.parse_authorization_header(auth_header)
# Ignore foreign Authorization headers.
if method.lower() != 'signature':
return None
# Verify basic header structure.
if len(fields) == 0:
raise FAILED
# Ensure all required fields were included.
if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0:
raise FAILED
# Fetch the secret associated with the keyid
user, secret = self.fetch_user_data(
fields["keyid"],
algorithm=fields["algorithm"]
)
if not (user and secret):
raise FAILED
# Gather all request headers and translate them as stated in the Django docs:
# https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META
headers = {}
for key in request.META.keys():
if key.startswith("HTTP_") or \
key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
header = key[5:].lower().replace('_', '-')
headers[header] = request.META[key]
# Verify headers
hs = HeaderVerifier(
headers,
secret,
required_headers=self.required_headers,
method=request.method.lower(),
path=request.get_full_path()
)
# All of that just to get to this.
if not hs.verify():
raise FAILED
return user, fields["keyid"]

View File

@ -1,9 +1,14 @@
# -*- coding: utf-8 -*-
#
from .mixins import BulkListSerializerMixin
from rest_framework_bulk.serializers import BulkListSerializer
from rest_framework import serializers
from .mixins import BulkListSerializerMixin
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
pass
class CeleryTaskSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True)

View File

@ -29,6 +29,6 @@ def send_mail_async(*args, **kwargs):
args = tuple(args)
try:
send_mail(*args, **kwargs)
return send_mail(*args, **kwargs)
except Exception as e:
logger.error("Sending mail error: {}".format(e))

View File

@ -31,6 +31,10 @@ def get_logger(name=None):
return logging.getLogger('jumpserver.%s' % name)
def get_syslogger(name=None):
return logging.getLogger('jms.%s' % name)
def timesince(dt, since='', default="just now"):
"""
Returns string representing "time since" e.g.

View File

@ -379,6 +379,8 @@ defaults = {
'ASSETS_PERM_CACHE_TIME': 3600*24,
'SECURITY_MFA_VERIFY_TTL': 3600,
'ASSETS_PERM_CACHE_ENABLE': False,
'SYSLOG_ADDR': '', # '192.168.0.1:514'
'SYSLOG_FACILITY': 'user',
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
}

View File

@ -217,6 +217,9 @@ LOGGING = {
'simple': {
'format': '%(levelname)s %(message)s'
},
'syslog': {
'format': 'jumpserver: %(message)s'
},
'msg': {
'format': '%(message)s'
}
@ -249,19 +252,10 @@ LOGGING = {
'backupCount': 7,
'filename': ANSIBLE_LOG_FILE,
},
'gunicorn_file': {
'encoding': 'utf8',
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'msg',
'maxBytes': 1024*1024*100,
'backupCount': 2,
'filename': GUNICORN_LOG_FILE,
},
'gunicorn_console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'msg'
'syslog': {
'level': 'INFO',
'class': 'logging.NullHandler',
'formatter': 'syslog'
},
},
'loggers': {
@ -271,25 +265,17 @@ LOGGING = {
'level': LOG_LEVEL,
},
'django.request': {
'handlers': ['console', 'file'],
'handlers': ['console', 'file', 'syslog'],
'level': LOG_LEVEL,
'propagate': False,
},
'django.server': {
'handlers': ['console', 'file'],
'handlers': ['console', 'file', 'syslog'],
'level': LOG_LEVEL,
'propagate': False,
},
'jumpserver': {
'handlers': ['console', 'file'],
'level': LOG_LEVEL,
},
'jumpserver.users.api': {
'handlers': ['console', 'file'],
'level': LOG_LEVEL,
},
'jumpserver.users.view': {
'handlers': ['console', 'file'],
'handlers': ['console', 'file', 'syslog'],
'level': LOG_LEVEL,
},
'ops.ansible_api': {
@ -300,17 +286,28 @@ LOGGING = {
'handlers': ['console', 'file'],
'level': "INFO",
},
'gunicorn': {
'handlers': ['gunicorn_console', 'gunicorn_file'],
'level': 'INFO',
'jms_audits': {
'handlers': ['syslog'],
'level': 'INFO'
},
# 'django.db': {
# 'handlers': ['console', 'file'],
# 'level': 'DEBUG'
# }
'django.db': {
'handlers': ['console', 'file'],
'level': 'DEBUG'
}
}
}
SYSLOG_ENABLE = False
if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2:
host, port = CONFIG.SYSLOG_ADDR.split(':')
SYSLOG_ENABLE = True
LOGGING['handlers']['syslog'].update({
'class': 'logging.handlers.SysLogHandler',
'facility': CONFIG.SYSLOG_FACILITY,
'address': (host, int(port)),
})
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
# LANGUAGE_CODE = 'en'
@ -391,6 +388,7 @@ REST_FRAMEWORK = {
'authentication.backends.api.AccessKeyAuthentication',
'authentication.backends.api.AccessTokenAuthentication',
'authentication.backends.api.PrivateTokenAuthentication',
'authentication.backends.api.SignatureAuthentication',
'authentication.backends.api.SessionAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
@ -403,7 +401,7 @@ REST_FRAMEWORK = {
'SEARCH_PARAM': "search",
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'],
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
# 'PAGE_SIZE': 15
}
@ -601,9 +599,12 @@ USER_GUIDE_URL = ""
SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema',
'USE_SESSION_AUTH': True,
'SECURITY_DEFINITIONS': {
'basic': {
'type': 'basic'
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
},
}

View File

@ -7,13 +7,44 @@ from drf_yasg import openapi
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
def get_tags(self, operation_keys):
if len(operation_keys) > 2 and operation_keys[1].startswith('v'):
return [operation_keys[2]]
if len(operation_keys) > 2:
return [operation_keys[0] + '_' + operation_keys[1]]
return super().get_tags(operation_keys)
def get_operation_id(self, operation_keys):
action = ''
dump_keys = [k for k in operation_keys]
if hasattr(self.view, 'action'):
action = self.view.action
if action == "bulk_destroy":
action = "bulk_delete"
if dump_keys[-2] == "children":
if self.path.find('id') < 0:
dump_keys.insert(-2, "root")
if dump_keys[0] == "perms" and dump_keys[1] == "users":
if self.path.find('{id}') < 0:
dump_keys.insert(2, "my")
if action.replace('bulk_', '') == dump_keys[-1]:
dump_keys[-1] = action
return super().get_operation_id(tuple(dump_keys))
def get_operation(self, operation_keys):
operation = super().get_operation(operation_keys)
operation.summary = operation.operation_id
return operation
def get_swagger_view(version='v1'):
from .urls import api_v1_patterns, api_v2_patterns
from .urls import api_v1, api_v2
from django.urls import path, include
api_v1_patterns = [
path('api/v1/', include(api_v1))
]
api_v2_patterns = [
path('api/v2/', include(api_v2))
]
if version == "v2":
patterns = api_v2_patterns
else:

View File

@ -7,26 +7,26 @@ from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog
from .views import IndexView, LunaView, I18NView, HealthCheckView
from .views import IndexView, LunaView, I18NView, HealthCheckView, redirect_format_api
from .swagger import get_swagger_view
api_v1 = [
path('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')),
path('applications/v1/', include('applications.urls.api_urls', namespace='api-applications')),
path('users/', include('users.urls.api_urls', namespace='api-users')),
path('assets/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/', include('common.urls.api_urls', namespace='api-common')),
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
]
api_v2 = [
path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
]
@ -48,30 +48,23 @@ if settings.XPACK_ENABLED:
path('xpack/', include('xpack.urls.view_urls', namespace='xpack'))
)
api_v1.append(
path('xpack/v1/', include('xpack.urls.api_urls', namespace='api-xpack'))
path('xpack/', include('xpack.urls.api_urls', namespace='api-xpack'))
)
js_i18n_patterns = i18n_patterns(
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
)
api_v1_patterns = [
path('api/', include(api_v1))
]
api_v2_patterns = [
path('api/', include(api_v2))
]
urlpatterns = [
path('', IndexView.as_view(), name='index'),
path('', include(api_v2_patterns)),
path('', include(api_v1_patterns)),
path('api/v1/', include(api_v1)),
path('api/v2/', include(api_v2)),
re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', redirect_format_api),
path('api/health/', HealthCheckView.as_view(), name="health"),
path('luna/', LunaView.as_view(), name='luna-view'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('settings.urls.view_urls', namespace='settings')),
# path('api/v2/', include(api_v2_patterns)),
# External apps url
path('captcha/', include('captcha.urls')),

View File

@ -2,14 +2,13 @@ import datetime
import re
import time
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, JsonResponse
from django.conf import settings
from django.views.generic import TemplateView, View
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from django.shortcuts import redirect
from rest_framework.response import Response
from rest_framework.views import APIView
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
@ -208,7 +207,7 @@ class I18NView(View):
return response
api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
api_url_pattern = re.compile(r'^/api/(?P<app>\w+)/(?P<version>v\d)/(?P<extra>.*)$')
@csrf_exempt
@ -216,18 +215,16 @@ def redirect_format_api(request, *args, **kwargs):
_path, query = request.path, request.GET.urlencode()
matched = api_url_pattern.match(_path)
if matched:
version, app, extra = matched.groups()
_path = '/api/{app}/{version}/{extra}?{query}'.format(**{
"app": app, "version": version, "extra": extra,
"query": query
})
kwargs = matched.groupdict()
kwargs["query"] = query
_path = '/api/{version}/{app}/{extra}?{query}'.format(**kwargs).rstrip("?")
return HttpResponseTemporaryRedirect(_path)
else:
return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404)
return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404)
class HealthCheckView(APIView):
permission_classes = ()
def get(self, request):
return Response({"status": 1, "time": int(time.time())})
return JsonResponse({"status": 1, "time": int(time.time())})

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-07-31 16:35+0800\n"
"POT-Creation-Date: 2019-08-07 16:56+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
@ -78,7 +78,7 @@ msgstr "运行参数"
#: assets/forms/domain.py:15 assets/forms/label.py:13
#: assets/models/asset.py:318 assets/models/authbook.py:24
#: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:81
#: assets/serializers/system_user.py:30
#: assets/serializers/system_user.py:31
#: assets/templates/assets/admin_user_list.html:46
#: assets/templates/assets/domain_detail.html:60
#: assets/templates/assets/domain_list.html:26
@ -218,7 +218,7 @@ msgstr "参数"
#: perms/models/asset_permission.py:117 perms/models/base.py:41
#: perms/templates/perms/asset_permission_detail.html:98
#: perms/templates/perms/remote_app_permission_detail.html:90
#: users/models/user.py:372 users/serializers/v1.py:120
#: users/models/user.py:372 users/serializers/v1.py:119
#: users/templates/users/user_detail.html:111
#: xpack/plugins/change_auth_plan/models.py:106
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113
@ -471,6 +471,7 @@ msgstr "更新"
#: assets/templates/assets/label_list.html:40
#: assets/templates/assets/system_user_detail.html:30
#: assets/templates/assets/system_user_list.html:86 audits/models.py:34
#: authentication/templates/authentication/_access_key_modal.html:65
#: ops/templates/ops/task_list.html:64
#: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:179
@ -527,6 +528,7 @@ msgstr "创建远程应用"
#: assets/templates/assets/system_user_list.html:60 audits/models.py:38
#: audits/templates/audits/operate_log_list.html:41
#: audits/templates/audits/operate_log_list.html:67
#: authentication/templates/authentication/_access_key_modal.html:30
#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64
#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:34
#: perms/forms/asset_permission.py:21
@ -579,15 +581,11 @@ msgstr "远程应用详情"
msgid "My RemoteApp"
msgstr "我的远程应用"
#: assets/api/asset.py:51
#: assets/api/asset.py:42
#, python-format
msgid "%(hostname)s was %(action)s successfully"
msgstr "%(hostname)s %(action)s成功"
#: assets/api/asset.py:125
msgid "Please select assets that need to be updated"
msgstr "请选择需要更新的资产"
#: assets/api/node.py:61
msgid "You can't update the root node name"
msgstr "不能修改根节点名称"
@ -609,7 +607,7 @@ msgstr "不可达"
msgid "Reachable"
msgstr "可连接"
#: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:9
#: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:13
#: xpack/plugins/license/models.py:78
msgid "Unknown"
msgstr "未知"
@ -656,7 +654,7 @@ msgid "Domain"
msgstr "网域"
#: assets/forms/asset.py:58 assets/forms/asset.py:80 assets/forms/asset.py:93
#: assets/forms/asset.py:128 assets/models/node.py:253
#: assets/forms/asset.py:128 assets/models/node.py:254
#: assets/templates/assets/asset_create.html:42
#: perms/forms/asset_permission.py:72 perms/forms/asset_permission.py:79
#: perms/models/asset_permission.py:112
@ -718,7 +716,7 @@ msgstr "SSH网关支持代理SSH,RDP和VNC"
#: assets/templates/assets/admin_user_list.html:45
#: assets/templates/assets/domain_gateway_list.html:71
#: assets/templates/assets/system_user_detail.html:62
#: assets/templates/assets/system_user_list.html:52 audits/models.py:94
#: assets/templates/assets/system_user_list.html:52 audits/models.py:80
#: audits/templates/audits/login_log_list.html:51 authentication/forms.py:13
#: authentication/templates/authentication/login.html:65
#: authentication/templates/authentication/new_login.html:92
@ -1116,8 +1114,8 @@ msgstr "默认资产组"
#: terminal/templates/terminal/command_list.html:65
#: terminal/templates/terminal/session_list.html:27
#: terminal/templates/terminal/session_list.html:71 users/forms.py:316
#: users/models/user.py:128 users/models/user.py:458
#: users/serializers/v1.py:109 users/templates/users/user_group_detail.html:78
#: users/models/user.py:127 users/models/user.py:458
#: users/serializers/v1.py:108 users/templates/users/user_group_detail.html:78
#: users/templates/users/user_group_list.html:36 users/views/user.py:243
#: xpack/plugins/orgs/forms.py:26
#: xpack/plugins/orgs/templates/orgs/org_detail.html:113
@ -1125,7 +1123,7 @@ msgstr "默认资产组"
msgid "User"
msgstr "用户"
#: assets/models/label.py:19 assets/models/node.py:244
#: assets/models/label.py:19 assets/models/node.py:245
#: assets/templates/assets/label_list.html:15 settings/models.py:30
msgid "Value"
msgstr "值"
@ -1134,11 +1132,11 @@ msgstr "值"
msgid "Category"
msgstr "分类"
#: assets/models/node.py:243
#: assets/models/node.py:244
msgid "Key"
msgstr "键"
#: assets/models/node.py:301
#: assets/models/node.py:302
msgid "New node"
msgstr "新节点"
@ -1244,98 +1242,98 @@ msgstr "密钥不合法"
msgid "The same level node name cannot be the same"
msgstr "同级别节点名字不能重复"
#: assets/serializers/system_user.py:31
#: assets/serializers/system_user.py:32
msgid "Login mode display"
msgstr "登录模式显示"
#: assets/serializers/system_user.py:66
#: assets/serializers/system_user.py:67
msgid "* Automatic login mode must fill in the username."
msgstr "自动登录模式,必须填写用户名"
#: assets/serializers/system_user.py:75
#: assets/serializers/system_user.py:78
msgid "Password or private key required"
msgstr "密码或密钥密码需要一个"
#: assets/tasks.py:34
#: assets/tasks.py:33
msgid "Asset has been disabled, skipped: {}"
msgstr "资产或许不支持ansible, 跳过: {}"
#: assets/tasks.py:38
#: assets/tasks.py:37
msgid "Asset may not be support ansible, skipped: {}"
msgstr "资产或许不支持ansible, 跳过: {}"
#: assets/tasks.py:51
#: assets/tasks.py:50
msgid "No assets matched, stop task"
msgstr "没有匹配到资产,结束任务"
#: assets/tasks.py:61
#: assets/tasks.py:60
msgid "No assets matched related system user protocol, stop task"
msgstr "没有匹配到与系统用户协议相关的资产,结束任务"
#: assets/tasks.py:87
#: assets/tasks.py:86
msgid "Get asset info failed: {}"
msgstr "获取资产信息失败:{}"
#: assets/tasks.py:137
#: assets/tasks.py:136
msgid "Update some assets hardware info"
msgstr "更新资产硬件信息"
#: assets/tasks.py:154
#: assets/tasks.py:153
msgid "Update asset hardware info: {}"
msgstr "更新资产硬件信息: {}"
#: assets/tasks.py:179
#: assets/tasks.py:178
msgid "Test assets connectivity"
msgstr "测试资产可连接性"
#: assets/tasks.py:233
#: assets/tasks.py:232
msgid "Test assets connectivity: {}"
msgstr "测试资产可连接性: {}"
#: assets/tasks.py:275
#: assets/tasks.py:274
msgid "Test admin user connectivity period: {}"
msgstr "定期测试管理账号可连接性: {}"
#: assets/tasks.py:282
#: assets/tasks.py:281
msgid "Test admin user connectivity: {}"
msgstr "测试管理行号可连接性: {}"
#: assets/tasks.py:350
#: assets/tasks.py:349
msgid "Test system user connectivity: {}"
msgstr "测试系统用户可连接性: {}"
#: assets/tasks.py:357
#: assets/tasks.py:356
msgid "Test system user connectivity: {} => {}"
msgstr "测试系统用户可连接性: {} => {}"
#: assets/tasks.py:370
#: assets/tasks.py:369
msgid "Test system user connectivity period: {}"
msgstr "定期测试系统用户可连接性: {}"
#: assets/tasks.py:479 assets/tasks.py:565
#: assets/tasks.py:478 assets/tasks.py:564
#: xpack/plugins/change_auth_plan/models.py:522
msgid "The asset {} system platform {} does not support run Ansible tasks"
msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务"
#: assets/tasks.py:491
#: assets/tasks.py:490
msgid ""
"Push system user task skip, auto push not enable or protocol is not ssh or "
"rdp: {}"
msgstr "推送系统用户任务跳过自动推送没有打开或协议不是ssh或rdp: {}"
#: assets/tasks.py:498
#: assets/tasks.py:497
msgid "For security, do not push user {}"
msgstr "为了安全,禁止推送用户 {}"
#: assets/tasks.py:526 assets/tasks.py:540
#: assets/tasks.py:525 assets/tasks.py:539
msgid "Push system users to assets: {}"
msgstr "推送系统用户到入资产: {}"
#: assets/tasks.py:532
#: assets/tasks.py:531
msgid "Push system users to asset: {} => {}"
msgstr "推送系统用户到入资产: {} => {}"
#: assets/tasks.py:612
#: assets/tasks.py:611
msgid "Test asset user connectivity: {}"
msgstr "测试资产用户可连接性: {}"
@ -1418,6 +1416,7 @@ msgstr "获取认证信息错误"
#: assets/templates/assets/_asset_user_auth_view_modal.html:97
#: assets/templates/assets/_user_asset_detail_modal.html:23
#: authentication/templates/authentication/_access_key_modal.html:143
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
#: settings/templates/settings/_ldap_list_users_modal.html:99
#: templates/_modal.html:22
@ -1701,7 +1700,8 @@ msgstr "硬盘"
msgid "Date joined"
msgstr "创建日期"
#: assets/templates/assets/asset_detail.html:150
#: assets/templates/assets/asset_detail.html:150 authentication/models.py:15
#: authentication/templates/authentication/_access_key_modal.html:28
#: perms/models/asset_permission.py:115 perms/models/base.py:38
#: perms/templates/perms/asset_permission_create_update.html:55
#: perms/templates/perms/asset_permission_detail.html:120
@ -2133,7 +2133,7 @@ msgstr "操作"
msgid "Filename"
msgstr "文件名"
#: audits/models.py:23 audits/models.py:90
#: audits/models.py:23 audits/models.py:76
#: audits/templates/audits/ftp_log_list.html:76
#: ops/templates/ops/command_execution_list.html:65
#: ops/templates/ops/task_list.html:31
@ -2143,7 +2143,9 @@ msgstr "文件名"
msgid "Success"
msgstr "成功"
#: audits/models.py:32 xpack/plugins/vault/templates/vault/vault.html:46
#: audits/models.py:32
#: authentication/templates/authentication/_access_key_modal.html:38
#: xpack/plugins/vault/templates/vault/vault.html:46
msgid "Create"
msgstr "创建"
@ -2169,55 +2171,39 @@ msgstr "禁用"
msgid "Enabled"
msgstr "启用"
#: audits/models.py:72 audits/models.py:82
#: audits/models.py:72
msgid "-"
msgstr ""
#: audits/models.py:83
msgid "Username/password check failed"
msgstr "用户名/密码 校验失败"
#: audits/models.py:84
msgid "MFA authentication failed"
msgstr "MFA 认证失败"
#: audits/models.py:85
msgid "Username does not exist"
msgstr "用户名不存在"
#: audits/models.py:86
msgid "Password expired"
msgstr "密码过期"
#: audits/models.py:91 xpack/plugins/cloud/models.py:267
#: audits/models.py:77 xpack/plugins/cloud/models.py:267
#: xpack/plugins/cloud/models.py:290
msgid "Failed"
msgstr "失败"
#: audits/models.py:95
#: audits/models.py:81
msgid "Login type"
msgstr "登录方式"
#: audits/models.py:96
#: audits/models.py:82
msgid "Login ip"
msgstr "登录IP"
#: audits/models.py:97
#: audits/models.py:83
msgid "Login city"
msgstr "登录城市"
#: audits/models.py:98
#: audits/models.py:84
msgid "User agent"
msgstr "Agent"
#: audits/models.py:99 audits/templates/audits/login_log_list.html:56
#: audits/models.py:85 audits/templates/audits/login_log_list.html:56
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: users/forms.py:175 users/models/user.py:353
#: users/templates/users/first_login.html:45
msgid "MFA"
msgstr "MFA"
#: audits/models.py:100 audits/templates/audits/login_log_list.html:57
#: audits/models.py:86 audits/templates/audits/login_log_list.html:57
#: xpack/plugins/change_auth_plan/models.py:417
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15
#: xpack/plugins/cloud/models.py:281
@ -2225,14 +2211,14 @@ msgstr "MFA"
msgid "Reason"
msgstr "原因"
#: audits/models.py:101 audits/templates/audits/login_log_list.html:58
#: audits/models.py:87 audits/templates/audits/login_log_list.html:58
#: xpack/plugins/cloud/models.py:278 xpack/plugins/cloud/models.py:313
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65
msgid "Status"
msgstr "状态"
#: audits/models.py:102
#: audits/models.py:88
msgid "Date login"
msgstr "登录日期"
@ -2264,13 +2250,14 @@ msgstr "选择用户"
#: ops/templates/ops/command_execution_list.html:43
#: ops/templates/ops/command_execution_list.html:48
#: ops/templates/ops/task_list.html:13 ops/templates/ops/task_list.html:18
#: templates/_base_list.html:41 templates/_header_bar.html:8
#: templates/_base_list.html:41
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:52
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:48
msgid "Search"
msgstr "搜索"
#: audits/templates/audits/login_log_list.html:50
#: authentication/templates/authentication/_access_key_modal.html:26
#: ops/templates/ops/adhoc_detail.html:49
#: ops/templates/ops/adhoc_history_detail.html:49
#: ops/templates/ops/task_detail.html:56
@ -2289,6 +2276,7 @@ msgid "City"
msgstr "城市"
#: audits/templates/audits/login_log_list.html:59
#: authentication/templates/authentication/_access_key_modal.html:29
#: ops/templates/ops/task_list.html:32
msgid "Date"
msgstr "日期"
@ -2319,79 +2307,99 @@ msgstr "登录日志"
msgid "Command execution log"
msgstr "命令执行"
#: authentication/api/auth.py:49
#: authentication/api/auth.py:51 authentication/api/token.py:45
#: authentication/templates/authentication/login.html:52
#: authentication/templates/authentication/new_login.html:77
msgid "Log in frequently and try again later"
msgstr "登录频繁, 稍后重试"
#: authentication/api/auth.py:67
msgid "The user {} password has expired, please update."
msgstr "用户 {} 密码已经过期,请更新。"
#: authentication/api/auth.py:86
#: authentication/api/auth.py:76
msgid "Please carry seed value and conduct MFA secondary certification"
msgstr "请携带seed值, 进行MFA二次认证"
#: authentication/api/auth.py:166
#: authentication/api/auth.py:156
msgid "Please verify the user name and password first"
msgstr "请先进行用户名和密码验证"
#: authentication/api/auth.py:171
#: authentication/api/auth.py:161
msgid "MFA certification failed"
msgstr "MFA认证失败"
#: authentication/backends/api.py:52
#: authentication/api/token.py:80
msgid "MFA required"
msgstr ""
#: authentication/backends/api.py:53
msgid "Invalid signature header. No credentials provided."
msgstr ""
#: authentication/backends/api.py:55
#: authentication/backends/api.py:56
msgid "Invalid signature header. Signature string should not contain spaces."
msgstr ""
#: authentication/backends/api.py:62
#: authentication/backends/api.py:63
msgid "Invalid signature header. Format like AccessKeyId:Signature"
msgstr ""
#: authentication/backends/api.py:66
#: authentication/backends/api.py:67
msgid ""
"Invalid signature header. Signature string should not contain invalid "
"characters."
msgstr ""
#: authentication/backends/api.py:86 authentication/backends/api.py:102
#: authentication/backends/api.py:87 authentication/backends/api.py:103
msgid "Invalid signature."
msgstr ""
#: authentication/backends/api.py:93
#: authentication/backends/api.py:94
msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT"
msgstr ""
#: authentication/backends/api.py:98
#: authentication/backends/api.py:99
msgid "Expired, more than 15 minutes"
msgstr ""
#: authentication/backends/api.py:105
#: authentication/backends/api.py:106
msgid "User disabled."
msgstr "用户已禁用"
#: authentication/backends/api.py:120
#: authentication/backends/api.py:121
msgid "Invalid token header. No credentials provided."
msgstr ""
#: authentication/backends/api.py:123
#: authentication/backends/api.py:124
msgid "Invalid token header. Sign string should not contain spaces."
msgstr ""
#: authentication/backends/api.py:130
#: authentication/backends/api.py:131
msgid ""
"Invalid token header. Sign string should not contain invalid characters."
msgstr ""
#: authentication/backends/api.py:140
#: authentication/backends/api.py:141
msgid "Invalid token or cache refreshed."
msgstr ""
#: authentication/const.py:6
msgid "Username/password check failed"
msgstr "用户名/密码 校验失败"
#: authentication/const.py:7
msgid "MFA authentication failed"
msgstr "MFA 认证失败"
#: authentication/const.py:8
msgid "Username does not exist"
msgstr "用户名不存在"
#: authentication/const.py:9
msgid "Password expired"
msgstr "密码过期"
#: authentication/const.py:10
msgid "Disabled or expired"
msgstr "禁用或失效"
#: authentication/forms.py:21
msgid ""
"The username or password you entered is incorrect, please enter it again."
@ -2418,10 +2426,43 @@ msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
msgid "MFA code"
msgstr "MFA 验证码"
#: authentication/models.py:33
#: authentication/models.py:35
msgid "Private Token"
msgstr "ssh密钥"
#: authentication/templates/authentication/_access_key_modal.html:6
msgid "API key list"
msgstr "API Key列表"
#: authentication/templates/authentication/_access_key_modal.html:18
msgid "Using api key sign api header, every requests header difference"
msgstr "使用api key签名请求头每个请求的头部是不一样的"
#: authentication/templates/authentication/_access_key_modal.html:18
msgid "docs"
msgstr "文档"
#: authentication/templates/authentication/_access_key_modal.html:27
msgid "Secret"
msgstr "密文"
#: authentication/templates/authentication/_access_key_modal.html:48
msgid "Show"
msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66
#: users/models/user.py:288 users/templates/users/user_profile.html:94
#: users/templates/users/user_profile.html:163
#: users/templates/users/user_profile.html:166
msgid "Disable"
msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67
#: users/models/user.py:289 users/templates/users/user_profile.html:92
#: users/templates/users/user_profile.html:170
msgid "Enable"
msgstr "启用"
#: authentication/templates/authentication/_mfa_confirm_modal.html:5
msgid "MFA confirm"
msgstr "MFA确认"
@ -2480,7 +2521,7 @@ msgstr "改变世界,从一点点开始。"
#: authentication/templates/authentication/login.html:46
#: authentication/templates/authentication/login.html:73
#: authentication/templates/authentication/new_login.html:101
#: templates/_header_bar.html:101
#: templates/_header_bar.html:83
msgid "Login"
msgstr "登录"
@ -2550,20 +2591,20 @@ msgstr "如果不能提供MFA验证码请联系管理员!"
msgid "Welcome back, please enter username and password to login"
msgstr "欢迎回来,请输入用户名和密码登录"
#: authentication/views/login.py:80
#: authentication/views/login.py:81
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: authentication/views/login.py:173 users/views/user.py:386
#: authentication/views/login.py:174 users/views/user.py:386
#: users/views/user.py:411
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确或者服务器端时间不对"
#: authentication/views/login.py:204
#: authentication/views/login.py:205
msgid "Logout success"
msgstr "退出登录成功"
#: authentication/views/login.py:205
#: authentication/views/login.py:206
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
@ -2642,11 +2683,11 @@ msgstr "不能包含特殊字符"
msgid "This field must be unique."
msgstr "字段必须唯一"
#: jumpserver/views.py:189 templates/_nav.html:4 templates/_nav_audits.html:4
#: jumpserver/views.py:188 templates/_nav.html:4 templates/_nav_audits.html:4
msgid "Dashboard"
msgstr "仪表盘"
#: jumpserver/views.py:198
#: jumpserver/views.py:197
msgid ""
"<div>Luna is a separately deployed program, you need to deploy Luna, coco, "
"configure nginx for url distribution,</div> </div>If you see this page, "
@ -3298,21 +3339,21 @@ msgstr "连接LDAP成功"
msgid "Match {} s users"
msgstr "匹配 {} 个用户"
#: settings/api.py:160
#: settings/api.py:161
msgid "succeed: {} failed: {} total: {}"
msgstr "成功:{} 失败:{} 总数:{}"
#: settings/api.py:182 settings/api.py:218
#: settings/api.py:183 settings/api.py:219
msgid ""
"Error: Account invalid (Please make sure the information such as Access key "
"or Secret key is correct)"
msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)"
#: settings/api.py:188 settings/api.py:224
#: settings/api.py:189 settings/api.py:225
msgid "Create succeed"
msgstr "创建成功"
#: settings/api.py:206 settings/api.py:244
#: settings/api.py:207 settings/api.py:245
#: settings/templates/settings/terminal_setting.html:154
msgid "Delete succeed"
msgstr "删除成功"
@ -3846,19 +3887,19 @@ msgstr "创建录像存储"
msgid "Create command storage"
msgstr "创建命令存储"
#: templates/_header_bar.html:31
#: templates/_header_bar.html:12
msgid "Help"
msgstr "帮助"
#: templates/_header_bar.html:38 users/templates/users/_base_otp.html:29
#: templates/_header_bar.html:19 users/templates/users/_base_otp.html:29
msgid "Docs"
msgstr "文档"
#: templates/_header_bar.html:44
#: templates/_header_bar.html:25
msgid "Commercial support"
msgstr "商业支持"
#: templates/_header_bar.html:89 templates/_nav_user.html:32 users/forms.py:154
#: templates/_header_bar.html:70 templates/_nav_user.html:32 users/forms.py:154
#: users/templates/users/_user.html:43
#: users/templates/users/first_login.html:39
#: users/templates/users/user_password_update.html:40
@ -3869,15 +3910,19 @@ msgstr "商业支持"
msgid "Profile"
msgstr "个人信息"
#: templates/_header_bar.html:92
#: templates/_header_bar.html:73
msgid "Admin page"
msgstr "管理页面"
#: templates/_header_bar.html:94
#: templates/_header_bar.html:75
msgid "User page"
msgstr "用户页面"
#: templates/_header_bar.html:97
#: templates/_header_bar.html:78
msgid "API Key"
msgstr ""
#: templates/_header_bar.html:79
msgid "Logout"
msgstr "注销登录"
@ -4413,7 +4458,7 @@ msgid ""
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
msgstr "你可以使用ssh客户端工具连接终端"
#: users/api/user.py:97
#: users/api/user.py:96
msgid "You do not have permission."
msgstr "你没有权限"
@ -4450,7 +4495,7 @@ msgstr "添加到用户组"
msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:95
#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:94
msgid "Not a valid ssh public key"
msgstr "ssh密钥不合法"
@ -4540,29 +4585,28 @@ msgstr "选择用户"
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
#: users/models/user.py:127 users/models/user.py:466
#: users/models/user.py:126 users/models/user.py:466
msgid "Administrator"
msgstr "管理员"
#: users/models/user.py:129
#: users/models/user.py:128
msgid "Application"
msgstr "应用程序"
#: users/models/user.py:130
#: users/models/user.py:129
msgid "Auditor"
msgstr "审计员"
#: users/models/user.py:288 users/templates/users/user_profile.html:94
#: users/templates/users/user_profile.html:163
#: users/templates/users/user_profile.html:166
msgid "Disable"
msgstr "禁用"
#: users/models/user.py:289 users/templates/users/user_profile.html:92
#: users/templates/users/user_profile.html:170
msgid "Enable"
msgstr "启用"
# #: users/models/user.py:288 users/templates/users/user_profile.html:94
# #: users/templates/users/user_profile.html:163
# #: users/templates/users/user_profile.html:166
# msgid "Disable"
# msgstr "禁用"
#
# #: users/models/user.py:289 users/templates/users/user_profile.html:92
# #: users/templates/users/user_profile.html:170
# msgid "Enable"
# msgstr "启用"
#: users/models/user.py:290 users/templates/users/user_profile.html:90
msgid "Force enable"
msgstr "强制启用"
@ -4589,39 +4633,39 @@ msgstr "最后更新密码日期"
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
#: users/serializers/v1.py:41
#: users/serializers/v1.py:39
msgid "Groups name"
msgstr "用户组名"
#: users/serializers/v1.py:42
#: users/serializers/v1.py:40
msgid "Source name"
msgstr "用户来源名"
#: users/serializers/v1.py:43
#: users/serializers/v1.py:41
msgid "Is first login"
msgstr "首次登录"
#: users/serializers/v1.py:44
#: users/serializers/v1.py:42
msgid "Role name"
msgstr "角色名"
#: users/serializers/v1.py:45
#: users/serializers/v1.py:43
msgid "Is valid"
msgstr "账户是否有效"
#: users/serializers/v1.py:46
#: users/serializers/v1.py:44
msgid "Is expired"
msgstr " 是否过期"
#: users/serializers/v1.py:47
#: users/serializers/v1.py:45
msgid "Avatar url"
msgstr "头像路径"
#: users/serializers/v1.py:55
#: users/serializers/v1.py:54
msgid "Role limit to {}"
msgstr "角色只能为 {}"
#: users/serializers/v1.py:67
#: users/serializers/v1.py:66
msgid "Password does not match security rules"
msgstr "密码不满足安全规则"
@ -4753,7 +4797,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry"
msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry"
#: users/templates/users/reset_password.html:46
#: users/templates/users/user_detail.html:377 users/utils.py:88
#: users/templates/users/user_detail.html:377 users/utils.py:84
msgid "Reset password"
msgstr "重置密码"
@ -5069,7 +5113,7 @@ msgid ""
"corresponding private key."
msgstr "新的公钥已设置成功,请下载对应的私钥"
#: users/utils.py:28
#: users/utils.py:24
#, python-format
msgid ""
"\n"
@ -5114,16 +5158,16 @@ msgstr ""
" </p>\n"
" "
#: users/utils.py:63
#: users/utils.py:59
msgid "Create account successfully"
msgstr "创建账户成功"
#: users/utils.py:67
#: users/utils.py:63
#, python-format
msgid "Hello %(name)s"
msgstr "您好 %(name)s"
#: users/utils.py:90
#: users/utils.py:86
#, python-format
msgid ""
"\n"
@ -5167,11 +5211,11 @@ msgstr ""
" <br>\n"
" "
#: users/utils.py:121
#: users/utils.py:117
msgid "Security notice"
msgstr "安全通知"
#: users/utils.py:123
#: users/utils.py:119
#, python-format
msgid ""
"\n"
@ -5220,11 +5264,11 @@ msgstr ""
" <br>\n"
" "
#: users/utils.py:159
#: users/utils.py:155
msgid "Expiration notice"
msgstr "过期通知"
#: users/utils.py:161
#: users/utils.py:157
#, python-format
msgid ""
"\n"
@ -5246,11 +5290,11 @@ msgstr ""
" <br>\n"
" "
#: users/utils.py:180
#: users/utils.py:176
msgid "SSH Key Reset"
msgstr "重置ssh密钥"
#: users/utils.py:182
#: users/utils.py:178
#, python-format
msgid ""
"\n"
@ -5275,18 +5319,6 @@ msgstr ""
" <br>\n"
" "
#: users/utils.py:215
msgid "User not exist"
msgstr "用户不存在"
#: users/utils.py:217
msgid "Disabled or expired"
msgstr "禁用或失效"
#: users/utils.py:230
msgid "Password or SSH public key invalid"
msgstr "密码或密钥不合法"
#: users/views/group.py:29
msgid "User group list"
msgstr "用户组列表"
@ -5718,12 +5750,16 @@ msgid "Create account"
msgstr "创建账户"
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:33
#, fuzzy
#| msgid "Instance"
msgid "Region & Instance"
msgstr "地域 & 实例"
msgstr "实例"
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37
#, fuzzy
#| msgid "Admin user"
msgid "Node & AdminUser"
msgstr "节点 & 管理用户"
msgstr "管理用户"
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:66
msgid "Loading..."
@ -5791,6 +5827,8 @@ msgstr "执行次数"
msgid "Instance count"
msgstr "实例个数"
# msgid "Sync success"
# msgstr "同步成功"
#: xpack/plugins/cloud/views.py:63
msgid "Update account"
msgstr "更新账户"
@ -6034,15 +6072,23 @@ msgstr "密码匣子"
msgid "vault create"
msgstr "创建"
#~ msgid "User not exist"
#~ msgstr "用户不存在"
# msgid "Disabled or expired"
# msgstr "禁用或失效"
#~ msgid "Password or SSH public key invalid"
#~ msgstr "密码或密钥不合法"
#~ msgid "Please select assets that need to be updated"
#~ msgstr "请选择需要更新的资产"
#~ msgid "Interface"
#~ msgstr "界面"
#~ msgid "Orgs"
#~ msgstr "组织管理"
#~ msgid "Org"
#~ msgstr "组织"
#~ msgid "already exists"
#~ msgstr "已经存在"

View File

@ -2,6 +2,7 @@
import datetime
import json
import os
from collections import defaultdict
from ansible import constants as C
@ -41,7 +42,11 @@ class CallbackMixin:
super().__init__()
if display:
self._display = display
cols = os.environ.get("TERM_COLS", None)
self._display.columns = 79
if cols and cols.isdigit():
self._display.columns = int(cols) - 1
def display(self, msg):
self._display.display(msg)

View File

@ -6,6 +6,7 @@ from rest_framework import viewsets, generics
from rest_framework.views import Response
from common.permissions import IsOrgAdmin
from common.serializers import CeleryTaskSerializer
from orgs.utils import current_org
from ..models import Task, AdHoc, AdHocRunHistory
from ..serializers import TaskSerializer, AdHocSerializer, \
@ -33,7 +34,7 @@ class TaskViewSet(viewsets.ModelViewSet):
class TaskRun(generics.RetrieveAPIView):
queryset = Task.objects.all()
# serializer_class = TaskViewSet
serializer_class = CeleryTaskSerializer
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from rest_framework.exceptions import ValidationError
from django.db import transaction
from django.utils.translation import ugettext as _
from django.conf import settings
from orgs.mixins import RootOrgViewMixin
from orgs.mixins.api import RootOrgViewMixin
from common.permissions import IsValidUser
from perms.utils import AssetPermissionUtilV2
from ..models import CommandExecution
from ..serializers import CommandExecutionSerializer
from ..tasks import run_command_execution
@ -20,15 +23,33 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet):
user_id=str(self.request.user.id)
)
def check_hosts(self, serializer):
data = serializer.validated_data
assets = data["hosts"]
system_user = data["run_as"]
util = AssetPermissionUtilV2(self.request.user)
util.filter_permissions(system_users=system_user.id)
permed_assets = util.get_assets().filter(id__in=[a.id for a in assets])
unpermed_assets = set(assets) - set(permed_assets)
if unpermed_assets:
msg = _("Not has host {} permission").format(
[str(a.id) for a in unpermed_assets]
)
raise ValidationError({"hosts": msg})
def check_permissions(self, request):
if not settings.SECURITY_COMMAND_EXECUTION and request.user.is_common_user:
return self.permission_denied(request, "Command execution disabled")
return super().check_permissions(request)
def perform_create(self, serializer):
self.check_hosts(serializer)
instance = serializer.save()
instance.user = self.request.user
instance.save()
cols = self.request.query_params.get("cols", '80')
rows = self.request.query_params.get("rows", '24')
transaction.on_commit(lambda: run_command_execution.apply_async(
args=(instance.id,), task_id=str(instance.id)
args=(instance.id,), kwargs={"cols": cols, "rows": rows},
task_id=str(instance.id)
))

View File

@ -2,6 +2,7 @@
import os
from kombu import Exchange, Queue
from celery import Celery
# set the default Django settings module for the 'celery' program.
@ -15,6 +16,14 @@ configs = {k: v for k, v in settings.__dict__.items() if k.startswith('CELERY')}
# Using a string here means the worker will not have to
# pickle the object when using Windows.
# app.config_from_object('django.conf:settings', namespace='CELERY')
configs["CELERY_QUEUES"] = [
Queue("celery", Exchange("celery"), routing_key="celery"),
Queue("ansible", Exchange("ansible"), routing_key="ansible"),
]
configs["CELERY_ROUTES"] = {
"ops.tasks.run_ansible_task": {'exchange': 'ansible', 'routing_key': 'ansible'},
}
app.namespace = 'CELERY'
app.conf.update(configs)
app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS])

View File

@ -30,8 +30,6 @@ class JMSBaseInventory(BaseInventory):
info.update(asset.get_auth_info())
if asset.is_unixlike():
info["become"] = asset.admin_user.become_info
for node in asset.nodes.all():
info["groups"].append(node.value)
if asset.is_windows():
info["vars"].update({
"ansible_connection": "ssh",
@ -45,7 +43,6 @@ class JMSBaseInventory(BaseInventory):
info["vars"].update({
"domain": asset.domain.name,
})
info["groups"].append("domain_"+asset.domain.name)
return info
@staticmethod

View File

@ -0,0 +1,28 @@
# Generated by Django 2.1.7 on 2019-07-24 12:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0006_auto_20190318_1023'),
]
operations = [
migrations.AlterField(
model_name='adhoc',
name='_become',
field=models.CharField(blank=True, default='', max_length=1024, verbose_name='Become'),
),
migrations.AlterField(
model_name='adhoc',
name='created_by',
field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='Create by'),
),
migrations.AlterField(
model_name='adhoc',
name='run_as',
field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='Username'),
),
]

View File

@ -161,9 +161,9 @@ class AdHoc(models.Model):
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
run_as = models.CharField(max_length=64, default='', null=True, verbose_name=_('Username'))
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username'))
_become = models.CharField(max_length=1024, default='', blank=True, verbose_name=_("Become"))
created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by'))
date_created = models.DateTimeField(auto_now_add=True, db_index=True)
@property

View File

@ -23,7 +23,7 @@ def rerun_task():
pass
@shared_task
@shared_task(queue="ansible")
def run_ansible_task(tid, callback=None, **kwargs):
"""
:param tid: is the tasks serialized data
@ -45,6 +45,10 @@ def run_command_execution(cid, **kwargs):
execution = get_object_or_none(CommandExecution, id=cid)
if execution:
try:
os.environ.update({
"TERM_ROWS": kwargs.get("rows", ""),
"TERM_COLS": kwargs.get("cols", ""),
})
execution.run()
except SoftTimeLimitExceeded:
logger.error("Run time out")
@ -98,7 +102,7 @@ def create_or_update_registered_periodic_tasks():
create_or_update_celery_periodic_tasks(task)
@shared_task
@shared_task(queue="ansible")
def hello(name, callback=None):
import time
time.sleep(10)
@ -109,7 +113,9 @@ def hello(name, callback=None):
# @after_app_shutdown_clean_periodic
# @register_as_period_task(interval=30)
def hello123():
p = subprocess.Popen('ls /tmp', shell=True)
print("{} Hello world".format(datetime.datetime.now().strftime("%H:%M:%S")))
return None
@shared_task

View File

@ -2,7 +2,7 @@
{% load i18n %}
<head>
<title>{% trans 'Task log' %}</title>
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
<style>

View File

@ -83,9 +83,50 @@
var zTree, show = 0;
var systemUserId = null;
var url = null;
var treeUrl = "{% url 'api-perms:my-nodes-assets-as-tree' %}?cache_policy=1";
var treeUrl = "{% url 'api-perms:my-nodes-children-with-assets-as-tree' %}?cache_policy=1";
function proposeGeometry(term) {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
var elementStyle = window.getComputedStyle(term.element);
var elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
var elementPaddingHor = elementPadding.right + elementPadding.left;
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor - term._core.viewport.scrollBarWidth;
var geometry = {
cols: Math.floor(availableWidth / term._core.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term._core.renderer.dimensions.actualCellHeight)
};
return geometry;
}
function fit(term) {
var geometry = proposeGeometry(term);
if (geometry) {
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term._core.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
}
function initTree() {
if (systemUserId) {
url = treeUrl + '&system_user=' + systemUserId
}
else{
url = treeUrl
}
var setting = {
check: {
enable: true
@ -99,6 +140,12 @@ function initTree() {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: {
enable: true,
showRemoveBtn: false,
@ -112,12 +159,7 @@ function initTree() {
onCheck: onCheck
}
};
if (systemUserId) {
url = treeUrl + '&system_user=' + systemUserId
}
else{
url = treeUrl
}
$.get(url, function(data, status){
$.fn.zTree.init($("#assetTree"), setting, data);
@ -183,6 +225,7 @@ function initResultTerminal() {
screenKeys: false,
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 14,
lineHeight: 1,
rightClickSelectsWord: true,
disableStdin: true,
theme: {
@ -190,7 +233,9 @@ function initResultTerminal() {
}
});
term.open(document.getElementById('term'));
term.write("{% trans 'Select the left asset, select the running system user, execute command in batch' %}" + "\r\n")
var msg = "{% trans 'Select the left asset, select the running system user, execute command in batch' %}" + "\r\n";
fit(term);
term.write(msg)
}
function wrapperError(msg) {
@ -201,7 +246,8 @@ function execute() {
if (!term) {
initResultTerminal()
}
var url = '{% url "api-ops:command-execution-list" %}';
var size = 'rows=' + term.rows + '&cols=' + term.cols;
var url = '{% url "api-ops:command-execution-list" %}?' + size;
var run_as = systemUserId;
var command = editor.getValue();
var hosts = getSelectedAssetsNode().map(function (node) {

View File

@ -65,9 +65,9 @@ class CommandExecutionStartView(PermissionsMixin, TemplateView):
return super().get_permissions()
def get_user_system_users(self):
from perms.utils import AssetPermissionUtil
from perms.utils import AssetPermissionUtilV2
user = self.request.user
util = AssetPermissionUtil(user)
util = AssetPermissionUtilV2(user)
system_users = [s for s in util.get_system_users() if s.protocol == 'ssh']
return system_users

View File

@ -14,7 +14,7 @@ from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission
from orgs.utils import current_org
from common.utils import get_logger
from .mixins import OrgMembershipModelViewSetMixin
from .mixins.api import OrgMembershipModelViewSetMixin
logger = get_logger(__file__)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
from .models import *
from .serializers import *
from .forms import *
from .api import *
# from .models import *
# from .serializers import *
# from .forms import *
# from .api import *

View File

@ -34,6 +34,9 @@ class OrgBulkModelViewSet(IDInCacheFilterMixin, BulkModelViewSet):
queryset = self.serializer_class.setup_eager_loading(queryset)
return queryset
def allow_bulk_destroy(self, qs, filtered):
return False
class OrgMembershipModelViewSetMixin:
org = None

View File

@ -8,7 +8,7 @@ from perms.models import AssetPermission
from common.serializers import AdaptedBulkListSerializer
from .utils import set_current_org, get_current_org
from .models import Organization
from .mixins import OrgMembershipSerializerMixin
from .mixins.serializers import OrgMembershipSerializerMixin
class OrgSerializer(ModelSerializer):

View File

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from django.urls import re_path
from rest_framework.routers import DefaultRouter
from common import api as capi
from .. import api
@ -10,20 +12,18 @@ app_name = 'orgs'
router = DefaultRouter()
# 将会删除
router.register(r'org/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMembershipAdminsViewSet, 'membership-admins')
router.register(r'org/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMembershipUsersViewSet, 'membership-users'),
# 替换为这个
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMembershipAdminsViewSet, 'membership-admins-2')
api.OrgMembershipAdminsViewSet, 'membership-admins')
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMembershipUsersViewSet, 'membership-users-2'),
api.OrgMembershipUsersViewSet, 'membership-users'),
router.register(r'orgs', api.OrgViewSet, 'org')
old_version_urlpatterns = [
re_path('(?P<resource>org)/.*', capi.redirect_plural_name_api)
]
urlpatterns = [
]
urlpatterns += router.urls
urlpatterns += router.urls + old_version_urlpatterns

View File

@ -7,7 +7,6 @@ from rest_framework.views import Response
from django.shortcuts import get_object_or_404
from rest_framework.generics import RetrieveUpdateAPIView, ListAPIView
from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdmin
from common.utils import get_object_or_none
@ -31,7 +30,6 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
"""
queryset = AssetPermission.objects.all()
serializer_class = serializers.AssetPermissionCreateUpdateSerializer
pagination_class = LimitOffsetPagination
filter_fields = ['name']
permission_classes = (IsOrgAdmin,)
@ -247,7 +245,6 @@ class AssetPermissionAddAssetApi(RetrieveUpdateAPIView):
class AssetPermissionAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
serializer_class = serializers.AssetPermissionAssetsSerializer
filter_fields = ("hostname", "ip")
search_fields = filter_fields

View File

@ -12,9 +12,6 @@ from django.views.decorators.http import condition
from django.utils.translation import ugettext as _
from common.utils import get_logger
from assets.utils import LabelFilterMixin
from ..utils import (
AssetPermissionUtil
)
from .. import const
from ..hands import Asset, Node, SystemUser
from .. import serializers
@ -24,119 +21,120 @@ logger = get_logger(__name__)
__all__ = ['UserPermissionCacheMixin', 'GrantAssetsMixin', 'NodesWithUngroupMixin']
def get_etag(request, *args, **kwargs):
cache_policy = request.GET.get("cache_policy")
if cache_policy != '1':
return None
if not UserPermissionCacheMixin.CACHE_ENABLE:
return None
view = request.parser_context.get("view")
if not view:
return None
etag = view.get_meta_cache_id()
return etag
# def get_etag(request, *args, **kwargs):
# cache_policy = request.GET.get("cache_policy")
# if cache_policy != '1':
# return None
# if not UserPermissionCacheMixin.CACHE_ENABLE:
# return None
# view = request.parser_context.get("view")
# if not view:
# return None
# etag = view.get_meta_cache_id()
# return etag
class UserPermissionCacheMixin:
cache_policy = '0'
RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_V2_{}'
CACHE_ENABLE = settings.ASSETS_PERM_CACHE_ENABLE
CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
_object = None
def get_object(self):
return None
# 内部使用可控制缓存
def _get_object(self):
if not self._object:
self._object = self.get_object()
return self._object
def get_object_id(self):
obj = self._get_object()
if obj:
return str(obj.id)
return None
def get_request_md5(self):
path = self.request.path
query = {k: v for k, v in self.request.GET.items()}
query.pop("_", None)
query = "&".join(["{}={}".format(k, v) for k, v in query.items()])
full_path = "{}?{}".format(path, query)
return md5(full_path.encode()).hexdigest()
def get_meta_cache_id(self):
obj = self._get_object()
util = AssetPermissionUtil(obj, cache_policy=self.cache_policy)
meta_cache_id = util.cache_meta.get('id')
return meta_cache_id
def get_response_cache_id(self):
obj_id = self.get_object_id()
request_md5 = self.get_request_md5()
meta_cache_id = self.get_meta_cache_id()
resp_cache_id = '{}_{}_{}'.format(obj_id, request_md5, meta_cache_id)
return resp_cache_id
def get_response_from_cache(self):
# 没有数据缓冲
meta_cache_id = self.get_meta_cache_id()
if not meta_cache_id:
logger.debug("Not get meta id: {}".format(meta_cache_id))
return None
# 从响应缓冲里获取响应
key = self.get_response_key()
data = cache.get(key)
if not data:
logger.debug("Not get response from cache: {}".format(key))
return None
logger.debug("Get user permission from cache: {}".format(self.get_object()))
response = Response(data)
return response
def expire_response_cache(self):
obj_id = self.get_object_id()
expire_cache_id = '{}_{}'.format(obj_id, '*')
key = self.RESP_CACHE_KEY.format(expire_cache_id)
cache.delete_pattern(key)
def get_response_key(self):
resp_cache_id = self.get_response_cache_id()
key = self.RESP_CACHE_KEY.format(resp_cache_id)
return key
def set_response_to_cache(self, response):
key = self.get_response_key()
cache.set(key, response.data, self.CACHE_TIME)
logger.debug("Set response to cache: {}".format(key))
@method_decorator(condition(etag_func=get_etag))
def get(self, request, *args, **kwargs):
if not self.CACHE_ENABLE:
self.cache_policy = '0'
else:
self.cache_policy = request.GET.get('cache_policy', '0')
obj = self._get_object()
if obj is None:
logger.debug("Not get response from cache: obj is none")
return super().get(request, *args, **kwargs)
if AssetPermissionUtil.is_not_using_cache(self.cache_policy):
logger.debug("Not get resp from cache: {}".format(self.cache_policy))
return super().get(request, *args, **kwargs)
elif AssetPermissionUtil.is_refresh_cache(self.cache_policy):
logger.debug("Not get resp from cache: {}".format(self.cache_policy))
self.expire_response_cache()
logger.debug("Try get response from cache")
resp = self.get_response_from_cache()
if not resp:
resp = super().get(request, *args, **kwargs)
self.set_response_to_cache(resp)
return resp
pass
# cache_policy = '0'
# RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_V2_{}'
# CACHE_ENABLE = settings.ASSETS_PERM_CACHE_ENABLE
# CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
# _object = None
#
# def get_object(self):
# return None
#
# # 内部使用可控制缓存
# def _get_object(self):
# if not self._object:
# self._object = self.get_object()
# return self._object
#
# def get_object_id(self):
# obj = self._get_object()
# if obj:
# return str(obj.id)
# return None
#
# def get_request_md5(self):
# path = self.request.path
# query = {k: v for k, v in self.request.GET.items()}
# query.pop("_", None)
# query = "&".join(["{}={}".format(k, v) for k, v in query.items()])
# full_path = "{}?{}".format(path, query)
# return md5(full_path.encode()).hexdigest()
#
# def get_meta_cache_id(self):
# obj = self._get_object()
# util = AssetPermissionUtil(obj, cache_policy=self.cache_policy)
# meta_cache_id = util.cache_meta.get('id')
# return meta_cache_id
#
# def get_response_cache_id(self):
# obj_id = self.get_object_id()
# request_md5 = self.get_request_md5()
# meta_cache_id = self.get_meta_cache_id()
# resp_cache_id = '{}_{}_{}'.format(obj_id, request_md5, meta_cache_id)
# return resp_cache_id
#
# def get_response_from_cache(self):
# # 没有数据缓冲
# meta_cache_id = self.get_meta_cache_id()
# if not meta_cache_id:
# logger.debug("Not get meta id: {}".format(meta_cache_id))
# return None
# # 从响应缓冲里获取响应
# key = self.get_response_key()
# data = cache.get(key)
# if not data:
# logger.debug("Not get response from cache: {}".format(key))
# return None
# logger.debug("Get user permission from cache: {}".format(self.get_object()))
# response = Response(data)
# return response
#
# def expire_response_cache(self):
# obj_id = self.get_object_id()
# expire_cache_id = '{}_{}'.format(obj_id, '*')
# key = self.RESP_CACHE_KEY.format(expire_cache_id)
# cache.delete_pattern(key)
#
# def get_response_key(self):
# resp_cache_id = self.get_response_cache_id()
# key = self.RESP_CACHE_KEY.format(resp_cache_id)
# return key
#
# def set_response_to_cache(self, response):
# key = self.get_response_key()
# cache.set(key, response.data, self.CACHE_TIME)
# logger.debug("Set response to cache: {}".format(key))
#
# @method_decorator(condition(etag_func=get_etag))
# def get(self, request, *args, **kwargs):
# if not self.CACHE_ENABLE:
# self.cache_policy = '0'
# else:
# self.cache_policy = request.GET.get('cache_policy', '0')
#
# obj = self._get_object()
# if obj is None:
# logger.debug("Not get response from cache: obj is none")
# return super().get(request, *args, **kwargs)
#
# if AssetPermissionUtil.is_not_using_cache(self.cache_policy):
# logger.debug("Not get resp from cache: {}".format(self.cache_policy))
# return super().get(request, *args, **kwargs)
# elif AssetPermissionUtil.is_refresh_cache(self.cache_policy):
# logger.debug("Not get resp from cache: {}".format(self.cache_policy))
# self.expire_response_cache()
#
# logger.debug("Try get response from cache")
# resp = self.get_response_from_cache()
# if not resp:
# resp = super().get(request, *args, **kwargs)
# self.set_response_to_cache(resp)
# return resp
class NodesWithUngroupMixin:
@ -202,9 +200,11 @@ class GrantAssetsMixin(LabelFilterMixin):
data.append(asset)
return data
def get_serializer(self, queryset_list, many=True):
data = self.get_serializer_queryset(queryset_list)
return super().get_serializer(data, many=True)
def get_serializer(self, assets_items=None, many=True):
if assets_items is None:
assets_items = []
assets_items = self.get_serializer_queryset(assets_items)
return super().get_serializer(assets_items, many=many)
def filter_queryset_by_id(self, assets_items):
i = self.request.query_params.get("id")

View File

@ -1,13 +1,10 @@
# coding: utf-8
#
from rest_framework import viewsets, generics
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import Response
from common.permissions import IsOrgAdmin
from ..models import RemoteAppPermission
from ..serializers import (
RemoteAppPermissionSerializer,
@ -28,7 +25,6 @@ class RemoteAppPermissionViewSet(viewsets.ModelViewSet):
search_fields = filter_fields
queryset = RemoteAppPermission.objects.all()
serializer_class = RemoteAppPermissionSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdmin,)

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