From 05438baad93214a1eb5262185bac93691aded73f Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 9 Jul 2019 12:05:31 +0800 Subject: [PATCH 01/19] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E6=98=BE=E7=A4=BA=E5=B7=AE8=E5=B0=8F=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/templates/assets/asset_create.html | 1 - apps/static/js/jumpserver.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index 15e75971e..1b5636be5 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -137,7 +137,6 @@ $(document).ready(function () { protocolRef.val(protocolShould); protocolRef.trigger("change") } - }) .on("click", ".btn-protocol.btn-del", function () { $(this).parent().parent().remove(); diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 352b4a654..a216a1ce0 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -1111,7 +1111,8 @@ function objectAttrsIsBool(obj, attrs) { function formatDateAsCN(d) { var date = new Date(d); - return date.toISOString().replace("T", " ").replace(/\..*/, ""); + var date_s = date.toLocaleString(navigator.language, {hour12: false}); + return date_s.split("/").join('-') } function getUrlParams(url) { From 6e86e3f1189ea3e5caa1fdff14b6d9cfc96a29a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Tue, 9 Jul 2019 17:30:53 +0800 Subject: [PATCH 02/19] =?UTF-8?q?[Update]=20=20=E5=88=9B=E5=BB=BA/?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20=E7=BD=91=E5=85=B3=E4=BD=BF=E7=94=A8api=20?= =?UTF-8?q?(#2911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 创建/更新 网关使用api * [Update] 修改小问题 * [Update] 修改小问题 * [Update] 修改网关一些信息可读 --- apps/assets/serializers/domain.py | 9 ++++--- .../assets/gateway_create_update.html | 26 +++++++++++++++++++ apps/assets/views/domain.py | 2 ++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index cda208b9f..68145e7a9 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -6,6 +6,7 @@ from common.serializers import AdaptedBulkListSerializer from orgs.mixins import BulkOrgResourceModelSerializer from ..models import Domain, Gateway +from .base import AuthSerializerMixin class DomainSerializer(BulkOrgResourceModelSerializer): @@ -26,14 +27,14 @@ class DomainSerializer(BulkOrgResourceModelSerializer): return obj.gateway_set.all().count() -class GatewaySerializer(BulkOrgResourceModelSerializer): +class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = Gateway list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'ip', 'port', 'protocol', 'username', - 'domain', 'is_active', 'date_created', 'date_updated', - 'created_by', 'comment', + 'id', 'name', 'ip', 'port', 'protocol', 'username', 'password', + 'private_key', 'public_key', 'domain', 'is_active', 'date_created', + 'date_updated', 'created_by', 'comment', ] diff --git a/apps/assets/templates/assets/gateway_create_update.html b/apps/assets/templates/assets/gateway_create_update.html index fea540385..a22428087 100644 --- a/apps/assets/templates/assets/gateway_create_update.html +++ b/apps/assets/templates/assets/gateway_create_update.html @@ -95,6 +95,32 @@ function protocolChange() { $(document).ready(function(){ protocolChange(); }) +.on("submit", "form", function (evt) { + evt.preventDefault(); + var form = $("form"); + var data = form.serializeObject(); + data["private_key"] = $("#id_private_key_file").data('file'); + var method = "POST"; + var the_url = '{% url "api-assets:gateway-list" %}'; + var redirect_to = '{% url "assets:domain-gateway-list" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", data.domain); + {% if type == "update" %} + the_url = '{% url 'api-assets:gateway-detail' pk=object.id %}'; + method = "PUT"; + {% endif %} + var props = { + url:the_url, + data:data, + method:method, + form:form, + redirect_to:redirect_to + }; + formSubmit(props); +}) +.on('change', '#id_private_key_file', function () { + readFile($(this)).on("onload", function (evt, data) { + $(this).attr("data-file", data) + }) +}) .on('change', protocol_id, function(){ protocolChange(); }); diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index 797bae1f4..6bbf09e25 100644 --- a/apps/assets/views/domain.py +++ b/apps/assets/views/domain.py @@ -132,6 +132,7 @@ class DomainGatewayCreateView(PermissionsMixin, CreateView): context = { 'app': _('Assets'), 'action': _('Create gateway'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -152,6 +153,7 @@ class DomainGatewayUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Assets'), 'action': _('Update gateway'), + "type": "update" } kwargs.update(context) return super().get_context_data(**kwargs) From ff85e2ef570ede3b5cdea42d1cdb1da1305291ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Thu, 11 Jul 2019 12:02:51 +0800 Subject: [PATCH 03/19] =?UTF-8?q?[Update]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E5=91=BD=E4=BB=A4=E8=BF=87=E6=BB=A4=E5=99=A8=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8api=20(#2926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/serializers/cmd_filter.py | 12 ++++++--- .../assets/cmd_filter_create_update.html | 26 +++++++++++++++++++ apps/assets/views/cmd_filter.py | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index e5ab62037..dfdff2cdb 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -10,13 +10,19 @@ from orgs.mixins import BulkOrgResourceModelSerializer class CommandFilterSerializer(BulkOrgResourceModelSerializer): - rules = serializers.PrimaryKeyRelatedField(queryset=CommandFilterRule.objects.all(), many=True) - system_users = serializers.PrimaryKeyRelatedField(queryset=SystemUser.objects.all(), many=True) class Meta: model = CommandFilter list_serializer_class = AdaptedBulkListSerializer - fields = '__all__' + fields = [ + 'id', 'name', 'org_id', 'org_name', 'is_active', 'comment', + 'created_by', 'date_created', 'date_updated', 'rules', 'system_users' + ] + + extra_kwargs = { + 'rules': {'read_only': True}, + 'system_users': {'read_only': True} + } class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): diff --git a/apps/assets/templates/assets/cmd_filter_create_update.html b/apps/assets/templates/assets/cmd_filter_create_update.html index b1f7a57a8..678e1e3eb 100644 --- a/apps/assets/templates/assets/cmd_filter_create_update.html +++ b/apps/assets/templates/assets/cmd_filter_create_update.html @@ -18,3 +18,29 @@ {% endblock %} +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/apps/assets/views/cmd_filter.py b/apps/assets/views/cmd_filter.py index 354c1d852..7eef5980a 100644 --- a/apps/assets/views/cmd_filter.py +++ b/apps/assets/views/cmd_filter.py @@ -47,6 +47,7 @@ class CommandFilterCreateView(PermissionsMixin, CreateView): context = { 'app': _('Assets'), 'action': _('Create command filter'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -64,6 +65,7 @@ class CommandFilterUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Assets'), 'action': _('Update command filter'), + 'type': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) From 5f9f970abdf78f481a4c0d26145da46cb143637b Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:12:14 +0800 Subject: [PATCH 04/19] Perf (#2929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 优化性能 * [Update] 修改assets * [Update] 优化处理 * [Update] Youhua * [Update] 修改ungroup * [Update] 修改perms的api地址,去掉sysuser adminuser的可连接性 * [Update] 修改perms urls兼容 * [Update] 修改分类 * [Update] 修改信号 * [Update] 优化获取授权资产 * [Update] 添加注释 (#2928) * [update] 去掉nodes 的部分字段 * [Update] 删除不用的代码 * [Update] 拆分users * [Update] 修改用户的属性 --- apps/assets/api/node.py | 2 +- apps/assets/models/asset.py | 65 ++- apps/assets/models/node.py | 4 +- apps/assets/models/user.py | 10 - apps/assets/serializers/admin_user.py | 3 +- apps/assets/serializers/node.py | 5 +- apps/assets/serializers/system_user.py | 3 +- apps/assets/signals_handler.py | 1 + apps/assets/templates/assets/_node_tree.html | 48 -- .../templates/assets/admin_user_list.html | 86 ++-- apps/assets/templates/assets/asset_list.html | 39 +- .../templates/assets/system_user_list.html | 84 +-- .../templates/assets/user_asset_list.html | 13 +- apps/assets/utils.py | 78 ++- apps/common/api.py | 10 + apps/common/http.py | 12 + apps/common/tree.py | 17 +- apps/common/utils/common.py | 16 + apps/jumpserver/views.py | 13 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 77611 -> 77406 bytes apps/locale/zh/LC_MESSAGES/django.po | 248 +++++---- apps/perms/api/remote_app_permission.py | 1 + apps/perms/api/user_group_permission.py | 137 +---- apps/perms/api/user_permission.py | 464 ++++++++++------- apps/perms/api/user_remote_app_permission.py | 21 +- apps/perms/const.py | 1 + apps/perms/models/asset_permission.py | 7 +- apps/perms/serializers/user_permission.py | 92 ++-- apps/perms/urls/api_urls.py | 69 ++- apps/perms/utils/asset_permission.py | 487 ++++++++++-------- apps/perms/utils/stack.py | 136 +++++ apps/users/models/user.py | 406 ++++++++------- .../templates/users/user_granted_asset.html | 12 +- 33 files changed, 1410 insertions(+), 1180 deletions(-) create mode 100644 apps/common/http.py create mode 100644 apps/perms/utils/stack.py diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 51f9a8739..2478303d0 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -131,7 +131,7 @@ class NodeChildrenAsTreeApi(generics.ListAPIView): if not include_assets: return queryset assets = self.node.get_assets().only( - "id", "hostname", "ip", 'platform', "os", "org_id", + "id", "hostname", "ip", 'platform', "os", "org_id", "protocols", ) for asset in assets: queryset.append(asset.as_tree_node(self.node)) diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index a37aa16b1..2161113e2 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -6,7 +6,8 @@ import uuid import logging import random from functools import reduce -from collections import OrderedDict +from collections import OrderedDict, defaultdict +from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -96,7 +97,53 @@ class ProtocolsMixin: return self.protocols_as_dict.get("ssh", 22) -class Asset(ProtocolsMixin, OrgModelMixin): +class NodesRelationMixin: + NODES_CACHE_KEY = 'ASSET_NODES_{}' + ALL_ASSET_NODES_CACHE_KEY = 'ALL_ASSETS_NODES' + CACHE_TIME = 3600 * 24 * 7 + id = "" + _all_nodes_keys = None + + @classmethod + def get_all_nodes_keys(cls): + """ + :return: {asset.id: [node.key, ]} + """ + from .node import Node + cache_key = cls.ALL_ASSET_NODES_CACHE_KEY + cached = cache.get(cache_key) + if cached: + return cached + assets = Asset.objects.all().only('id').prefetch_related( + models.Prefetch('nodes', queryset=Node.objects.all().only('key')) + ) + assets_nodes_keys = {} + for asset in assets: + assets_nodes_keys[asset.id] = [n.key for n in asset.nodes.all()] + cache.set(cache_key, assets_nodes_keys, cls.CACHE_TIME) + return assets_nodes_keys + + @classmethod + def expire_all_nodes_keys_cache(cls): + cache_key = cls.ALL_ASSET_NODES_CACHE_KEY + cache.delete(cache_key) + + def get_nodes(self): + from .node import Node + nodes = self.nodes.all() or [Node.root()] + return nodes + + def get_all_nodes(self, flat=False): + nodes = [] + for node in self.get_nodes(): + _nodes = node.get_ancestor(with_self=True) + nodes.append(_nodes) + if flat: + nodes = list(reduce(lambda x, y: set(x) | set(y), nodes)) + return nodes + + +class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): # Important PLATFORM_CHOICES = ( ('Linux', 'Linux'), @@ -182,20 +229,6 @@ class Asset(ProtocolsMixin, OrgModelMixin): def is_support_ansible(self): return self.has_protocol('ssh') and self.platform not in ("Other",) - def get_nodes(self): - from .node import Node - nodes = self.nodes.all() or [Node.root()] - return nodes - - def get_all_nodes(self, flat=False): - nodes = [] - for node in self.get_nodes(): - _nodes = node.get_ancestor(with_self=True) - nodes.append(_nodes) - if flat: - nodes = list(reduce(lambda x, y: set(x) | set(y), nodes)) - return nodes - @property def cpu_info(self): info = "" diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index aab5ebaf4..d668cea51 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -212,14 +212,12 @@ class AssetsAmountMixin: if cached is not None: return cached assets_amount = self.get_all_assets().count() - self.assets_amount = assets_amount + cache.set(cache_key, assets_amount, self.cache_time) return assets_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) def expire_assets_amount(self): ancestor_keys = self.get_ancestor_keys(with_self=True) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 32f569b80..bbd808b80 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -117,16 +117,6 @@ class SystemUser(AssetUser): def __str__(self): return '{0.name}({0.username})'.format(self) - def to_json(self): - return { - 'id': self.id, - 'name': self.name, - 'username': self.username, - 'protocol': self.protocol, - 'priority': self.priority, - 'auto_push': self.auto_push, - } - @property def login_mode_display(self): return self.get_login_mode_display() diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index b05de01f6..b8295457f 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -21,7 +21,7 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): model = AdminUser fields = [ 'id', 'name', 'username', 'password', 'private_key', 'public_key', - 'comment', 'connectivity_amount', 'assets_amount', + 'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by', ] @@ -33,7 +33,6 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'date_updated': {'read_only': True}, 'created_by': {'read_only': True}, 'assets_amount': {'label': _('Asset')}, - 'connectivity_amount': {'label': _('Connectivity')}, } diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index b33f22116..ea4aa7355 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -17,9 +17,8 @@ class NodeSerializer(BulkOrgResourceModelSerializer): class Meta: model = Node - fields = [ - 'id', 'key', 'value', 'assets_amount', 'org_id', - ] + only_fields = ['id', 'key', 'value', 'org_id'] + fields = only_fields + ['assets_amount'] read_only_fields = [ 'key', 'assets_amount', 'org_id', ] diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index d853984e2..70855c9f7 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -21,14 +21,13 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'id', 'name', 'username', 'password', 'public_key', 'private_key', 'login_mode', 'login_mode_display', 'priority', 'protocol', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'nodes', - 'assets_amount', 'connectivity_amount', 'auto_generate_key' + 'assets_amount', 'auto_generate_key' ] extra_kwargs = { 'password': {"write_only": True}, 'public_key': {"write_only": True}, 'private_key': {"write_only": True}, 'assets_amount': {'label': _('Asset')}, - 'connectivity_amount': {'label': _('Connectivity')}, 'login_mode_display': {'label': _('Login mode display')}, 'created_by': {'read_only': True}, } diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 59d01c98a..b2316d7d0 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -78,6 +78,7 @@ def on_system_user_assets_change(sender, instance=None, **kwargs): @receiver(m2m_changed, sender=Asset.nodes.through) 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']) diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index 93aed7d89..61737184c 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -291,42 +291,6 @@ function defaultCallback(action) { $(document).ready(function () { }) -.on('click', '.btn-refresh-hardware', function () { - var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}"; - var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id); - function success(data) { - rMenu.css({"visibility" : "hidden"}); - var task_id = data.task; - var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); - window.open(url, '', 'width=800,height=600') - } - APIUpdateAttr({ - url: the_url, - method: "GET", - success: success, - flash_message: false - }); - -}) -.on('click', '.btn-test-connective', function () { - var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}"; - if (!current_node_id) { - return null; - } - var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id); - function success(data) { - rMenu.css({"visibility" : "hidden"}); - var task_id = data.task; - var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); - window.open(url, '', 'width=800,height=600') - } - APIUpdateAttr({ - url: the_url, - method: "GET", - success: success, - flash_message: false - }); -}) .on('click', '.btn-show-current-asset', function(){ hideRMenu(); $(this).css('display', 'none'); @@ -341,17 +305,5 @@ $(document).ready(function () { setCookie('show_current_asset', ''); location.reload(); }) -.on('click', '.btn-test-connective', function () { - hideRMenu(); -}) -.on('click', '#menu_refresh_assets_amount', function () { - hideRMenu(); - var url = "{% url 'api-assets:refresh-assets-amount' %}"; - APIUpdateAttr({ - 'url': url, - 'method': 'GET' - }); - window.location.reload(); -}) \ No newline at end of file diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index 0a17eccaa..61389ad08 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -2,8 +2,6 @@ {% load i18n static %} {% block help_message %}
-{# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#} -{# Windows或其它硬件可以随意设置一个#} {% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%} {% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%} {% trans 'You can set any one for Windows or other hardware.' %} @@ -47,9 +45,9 @@ {% trans 'Name' %} {% trans 'Username' %} {% trans 'Asset' %} - {% trans 'Reachable' %} - {% trans 'Unreachable' %} - {% trans 'Ratio' %} +{# {% trans 'Reachable' %}#} +{# {% trans 'Unreachable' %}#} +{# {% trans 'Ratio' %}#} {% trans 'Comment' %} {% trans 'Action' %} @@ -73,44 +71,44 @@ function initTable() { var detail_btn = '' + cellData + ''; return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id); }}, - {targets: 4, createdCell: function (td, cellData) { - var innerHtml = ""; - var data = cellData.reachable; - if (data !== 0) { - innerHtml = "" + data + ""; - } else { - innerHtml = "" + data + ""; - } - $(td).html(innerHtml) - }}, - {targets: 5, createdCell: function (td, cellData) { - var data = cellData.unreachable; - var innerHtml = ""; - if (data !== 0) { - innerHtml = "" + data + ""; - } else { - innerHtml = "" + data + ""; - } - $(td).html('' + innerHtml + ''); - }}, - {targets: 6, createdCell: function (td, cellData, rowData) { - var val = 0; - var innerHtml = ""; - var total = rowData.assets_amount; - var reachable = cellData.reachable; - if (total !== 0) { - val = reachable/total * 100; - } - - if (val === 100) { - innerHtml = "" + val + "% "; - } else { - var num = new Number(val); - innerHtml = "" + num.toFixed(1) + "% "; - } - $(td).html('' + innerHtml + ''); - }}, - {targets: 8, createdCell: function (td, cellData, rowData) { + {#{targets: 4, createdCell: function (td, cellData) {#} + {# var innerHtml = "";#} + {# var data = cellData.reachable;#} + {# if (data !== 0) {#} + {# innerHtml = "" + data + "";#} + {# } else {#} + {# innerHtml = "" + data + "";#} + {# }#} + {# $(td).html(innerHtml)#} + {#}},#} + {#{targets: 5, createdCell: function (td, cellData) {#} + {# var data = cellData.unreachable;#} + {# var innerHtml = "";#} + {# if (data !== 0) {#} + {# innerHtml = "" + data + "";#} + {# } else {#} + {# innerHtml = "" + data + "";#} + {# }#} + {# $(td).html('' + innerHtml + '');#} + {#}},#} + {#{targets: 6, createdCell: function (td, cellData, rowData) {#} + {# var val = 0;#} + {# var innerHtml = "";#} + {# var total = rowData.assets_amount;#} + {# var reachable = cellData.reachable;#} + {# if (total !== 0) {#} + {# val = reachable/total * 100;#} + {# }#} + {##} + {# if (val === 100) {#} + {# innerHtml = "" + val + "% ";#} + {# } else {#} + {# var num = new Number(val);#} + {# innerHtml = "" + num.toFixed(1) + "% ";#} + {# }#} + {# $(td).html('' + innerHtml + '');#} + {#}},#} + {targets: 5, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); $(td).html(update_btn + del_btn) @@ -118,7 +116,7 @@ function initTable() { ajax_url: '{% url "api-assets:admin-user-list" %}', columns: [ {data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" }, - {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"}, + {#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#} {data: "comment"}, {data: "id"} ] }; diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 3b9df3373..8f889feb7 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -167,7 +167,7 @@ function initTable() { }}, {targets: 5, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace("{{ DEFAULT_PK }}", cellData); - var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); + var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); $(td).html(update_btn + del_btn) }} ], @@ -325,8 +325,7 @@ $(document).ready(function(){ } window.open(url, '_self'); }) - -.on('click', '.btn_asset_delete', function () { +.on('click', '.btn-asset-delete', function () { var $this = $(this); var $data_table = $("#asset_list_table").DataTable(); var name = $(this).closest("tr").find(":nth-child(2)").children('a').html(); @@ -513,6 +512,40 @@ $(document).ready(function(){ update_node_action = "add" }).on('click', '#menu_asset_move', function () { update_node_action = "move" +}).on('click', '.btn-test-connective', function () { + var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}"; + if (!current_node_id) { + return null; + } + var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id); + function success(data) { + rMenu.css({"visibility" : "hidden"}); + var task_id = data.task; + var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); + window.open(url, '', 'width=800,height=600') + } + APIUpdateAttr({ + url: the_url, + method: "GET", + success: success, + flash_message: false + }); +}).on('click', '.btn-refresh-hardware', function () { + var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}"; + var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id); + function success(data) { + rMenu.css({"visibility" : "hidden"}); + var task_id = data.task; + var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); + window.open(url, '', 'width=800,height=600') + } + APIUpdateAttr({ + url: the_url, + method: "GET", + success: success, + flash_message: false + }); + }) diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 680bfa421..6c5ff3339 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -53,9 +53,9 @@ {% trans 'Protocol' %} {% trans 'Login mode' %} {% trans 'Asset' %} - {% trans 'Reachable' %} - {% trans 'Unreachable' %} - {% trans 'Ratio' %} +{# {% trans 'Reachable' %}#} +{# {% trans 'Unreachable' %}#} +{# {% trans 'Ratio' %}#} {% trans 'Comment' %} {% trans 'Action' %} @@ -78,44 +78,44 @@ function initTable() { var detail_btn = '' + cellData + ''; $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); }}, - {targets: 6, createdCell: function (td, cellData) { - var innerHtml = ""; - var data = cellData.reachable; - if (data !== 0) { - innerHtml = "" + data + ""; - } else { - innerHtml = "" + data + ""; - } - $(td).html(innerHtml) - }}, - {targets: 7, createdCell: function (td, cellData) { - var data = cellData.unreachable; - var innerHtml = ""; - if (data !== 0) { - innerHtml = "" + data + ""; - } else { - innerHtml = "" + data + ""; - } - $(td).html('' + innerHtml + ''); - }}, - {targets: 8, createdCell: function (td, cellData, rowData) { - var val = 0; - var innerHtml = ""; - var total = rowData.assets_amount; - var reachable = cellData.reachable; - if (total && total !== 0) { - val = reachable/total * 100; - } - - if (val === 100) { - innerHtml = "" + val + "% "; - } else { - var num = new Number(val); - innerHtml = "" + num.toFixed(1) + "% "; - } - $(td).html('' + innerHtml + ''); - }}, - {targets: 10, createdCell: function (td, cellData, rowData) { + {#{targets: 6, createdCell: function (td, cellData) {#} + {# var innerHtml = "";#} + {# var data = cellData.reachable;#} + {# if (data !== 0) {#} + {# innerHtml = "" + data + "";#} + {# } else {#} + {# innerHtml = "" + data + "";#} + {# }#} + {# $(td).html(innerHtml)#} + {#}},#} + {#{targets: 7, createdCell: function (td, cellData) {#} + {# var data = cellData.unreachable;#} + {# var innerHtml = "";#} + {# if (data !== 0) {#} + {# innerHtml = "" + data + "";#} + {# } else {#} + {# innerHtml = "" + data + "";#} + {# }#} + {# $(td).html('' + innerHtml + '');#} + {#}},#} + {#{targets: 8, createdCell: function (td, cellData, rowData) {#} + {# var val = 0;#} + {# var innerHtml = "";#} + {# var total = rowData.assets_amount;#} + {# var reachable = cellData.reachable;#} + {# if (total && total !== 0) {#} + {# val = reachable/total * 100;#} + {# }#} + {##} + {# if (val === 100) {#} + {# innerHtml = "" + val + "% ";#} + {# } else {#} + {# var num = new Number(val);#} + {# innerHtml = "" + num.toFixed(1) + "% ";#} + {# }#} + {# $(td).html('' + innerHtml + '');#} + {#}},#} + {targets: 7, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); $(td).html(update_btn + del_btn) @@ -124,7 +124,7 @@ function initTable() { ajax_url: '{% url "api-assets:system-user-list" %}', columns: [ {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" }, - {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "comment" }, {data: "id" } + {data: "comment" }, {data: "id" } ], op_html: $('#actions').html() }; diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index 23d0b34ed..f441baa39 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -43,7 +43,6 @@ {% trans 'Hostname' %} {% trans 'IP' %} - {% trans 'Active' %} {% trans 'System users' %} {% trans 'Action' %} @@ -62,7 +61,7 @@ {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/views/group.py b/apps/users/views/group.py index f0e267de3..2f19a8055 100644 --- a/apps/users/views/group.py +++ b/apps/users/views/group.py @@ -44,6 +44,7 @@ class UserGroupCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): context = { 'app': _('Users'), 'action': _('Create user group'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -61,6 +62,7 @@ class UserGroupUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): context = { 'app': _('Users'), 'action': _('Update user group'), + 'type': 'update' } kwargs.update(context) From 63f3fa98db89fda70626392bcd405f543a0896a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:21:19 +0800 Subject: [PATCH 06/19] =?UTF-8?q?[Update]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E7=BD=91=E5=9F=9F=E4=BD=BF=E7=94=A8api=20(#2915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 创建/更新 网域使用api * [Update] 修改小问题 * [Update] 修改小问题 --- apps/assets/serializers/domain.py | 6 +++++- .../assets/domain_create_update.html | 21 +++++++++++++++++++ apps/assets/views/domain.py | 2 ++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 68145e7a9..68feccb41 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -15,7 +15,11 @@ class DomainSerializer(BulkOrgResourceModelSerializer): class Meta: model = Domain - fields = '__all__' + fields = [ + 'id', 'name', 'asset_count', 'gateway_count', 'comment', 'assets', + 'date_created' + ] + read_only_fields = ( 'asset_count', 'gateway_count', 'date_created') list_serializer_class = AdaptedBulkListSerializer @staticmethod diff --git a/apps/assets/templates/assets/domain_create_update.html b/apps/assets/templates/assets/domain_create_update.html index 7a31e3e88..399b38011 100644 --- a/apps/assets/templates/assets/domain_create_update.html +++ b/apps/assets/templates/assets/domain_create_update.html @@ -48,5 +48,26 @@ $(document).ready(function () { $("#asset_list_modal").modal('hide'); }) +.on("submit", "form", function (evt) { + evt.preventDefault(); + var form = $("form"); + var data = form.serializeObject(); + var method = "POST"; + var the_url = '{% url "api-assets:domain-list" %}'; + var redirect_to = '{% url "assets:domain-list" %}'; + {% if type == "update" %} + the_url = '{% url 'api-assets:domain-detail' pk=object.id %}'; + method = "PUT"; + {% endif %} + objectAttrsIsList(data, ['assets']); + var props = { + url:the_url, + data:data, + method:method, + form:form, + redirect_to:redirect_to + }; + formSubmit(props); + }) {% endblock %} \ No newline at end of file diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index 6bbf09e25..7b4dcfcce 100644 --- a/apps/assets/views/domain.py +++ b/apps/assets/views/domain.py @@ -46,6 +46,7 @@ class DomainCreateView(PermissionsMixin, CreateView): context = { 'app': _('Assets'), 'action': _('Create domain'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -63,6 +64,7 @@ class DomainUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Assets'), 'action': _('Update domain'), + 'type': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) From 95f1a19a0a7c903b81d7d44c16561b65f83ca035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:44:16 +0800 Subject: [PATCH 07/19] =?UTF-8?q?[Update]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E6=A0=87=E7=AD=BE=20=E4=BD=BF=E7=94=A8api=20(#2919?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 创建/更新 标签 使用api * [Update] 修改小问题 * [Update] 修改小问题 --- apps/assets/serializers/label.py | 8 ++++++- .../templates/assets/label_create_update.html | 21 +++++++++++++++++++ apps/assets/views/label.py | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index 526580216..a20c43a11 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -13,7 +13,13 @@ class LabelSerializer(BulkOrgResourceModelSerializer): class Meta: model = Label - fields = '__all__' + fields = [ + 'id', 'name', 'value', 'category', 'is_active', 'comment', + 'date_created', 'asset_count', 'assets', 'get_category_display' + ] + read_only_fields = ( + 'category', 'date_created', 'asset_count', 'get_category_display' + ) list_serializer_class = AdaptedBulkListSerializer @staticmethod diff --git a/apps/assets/templates/assets/label_create_update.html b/apps/assets/templates/assets/label_create_update.html index d55bb8827..a6b9582a5 100644 --- a/apps/assets/templates/assets/label_create_update.html +++ b/apps/assets/templates/assets/label_create_update.html @@ -51,5 +51,26 @@ $(document).ready(function () { $('#id_assets').val(assets).trigger('change'); $("#asset_list_modal").modal('hide'); }) +.on("submit", "form", function (evt) { + evt.preventDefault(); + var the_url = '{% url 'api-assets:label-list' %}'; + var redirect_to = '{% url "assets:label-list" %}'; + var method = "POST"; + {% if type == "update" %} + the_url = '{% url 'api-assets:label-detail' pk=object.id %}'; + method = "PUT"; + {% endif %} + var form = $("form"); + var data = form.serializeObject(); + objectAttrsIsList(data, ['assets']); + var props = { + url: the_url, + data: data, + method: method, + form: form, + redirect_to: redirect_to + }; + formSubmit(props); +}) {% endblock %} \ No newline at end of file diff --git a/apps/assets/views/label.py b/apps/assets/views/label.py index b53a5d040..522962ce3 100644 --- a/apps/assets/views/label.py +++ b/apps/assets/views/label.py @@ -44,6 +44,7 @@ class LabelCreateView(PermissionsMixin, CreateView): context = { 'app': _('Assets'), 'action': _('Create label'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -71,6 +72,7 @@ class LabelUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Assets'), 'action': _('Update label'), + 'type': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) From 48e74ed0ea8dcfbafe910fb40b40c26fa498e1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:45:18 +0800 Subject: [PATCH 08/19] =?UTF-8?q?[Bugfix]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7=E4=BD=BF?= =?UTF-8?q?=E7=94=A8api=20=E5=9C=A8=E9=80=89=E6=8B=A9=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=99=A8=E6=97=B6=E7=9A=84bug=20(#2924)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Bugfix] 创建/更新 系统用户使用api 在选择命令过滤器时的bug * [Update] 修改小问题 --- apps/assets/templates/assets/_system_user.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index ef78187b5..5cc3e04ba 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -228,6 +228,7 @@ $(document).ready(function () { var form = $("form"); var data = form.serializeObject(); + objectAttrsIsList(data, ['cmd_filters']); objectAttrsIsBool(data, ["auto_generate_key", "auto_push"]); data["private_key"] = $("#id_private_key_file").data('file'); From e0d492f59987be9f7a45efe44eaec9255cc329d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:50:22 +0800 Subject: [PATCH 09/19] =?UTF-8?q?[Update]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E8=BF=87=E6=BB=A4=E5=99=A8=E8=A7=84=E5=88=99=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8api=20(#2925)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 创建/更新 过滤器规则 使用api * [Update] 修改小问题 --- .../assets/cmd_filter_rule_create_update.html | 20 +++++++++++++++++++ apps/assets/views/cmd_filter.py | 3 +++ 2 files changed, 23 insertions(+) diff --git a/apps/assets/templates/assets/cmd_filter_rule_create_update.html b/apps/assets/templates/assets/cmd_filter_rule_create_update.html index 9b240bd74..2edaa97dc 100644 --- a/apps/assets/templates/assets/cmd_filter_rule_create_update.html +++ b/apps/assets/templates/assets/cmd_filter_rule_create_update.html @@ -70,5 +70,25 @@ $(document).ready(function(){ content_help_ref.html(content_origin_help_text); } }) +.on("submit", "form", function (evt) { + evt.preventDefault(); + var form = $("form"); + var data = form.serializeObject(); + var the_url = '{% url "api-assets:cmd-filter-rule-list" filter_pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", data.filter); + var redirect_to = '{% url "assets:cmd-filter-rule-list" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", data.filter); + var method = "POST"; + {% if request_type == "update" %} + the_url = '{% url "api-assets:cmd-filter-rule-detail" filter_pk=DEFAULT_PK pk=rule.id %}'.replace('{{ DEFAULT_PK }}', data.filter); + method = "PUT"; + {% endif %} + var props = { + url: the_url, + data: data, + method: method, + form: form, + redirect_to: redirect_to + }; + formSubmit(props); +}) {% endblock %} \ No newline at end of file diff --git a/apps/assets/views/cmd_filter.py b/apps/assets/views/cmd_filter.py index 7eef5980a..530f4193b 100644 --- a/apps/assets/views/cmd_filter.py +++ b/apps/assets/views/cmd_filter.py @@ -138,6 +138,7 @@ class CommandFilterRuleCreateView(PermissionsMixin, CreateView): 'app': _('Assets'), 'action': _('Create command filter rule'), 'object': self.cmd_filter, + 'request_type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -172,6 +173,8 @@ class CommandFilterRuleUpdateView(PermissionsMixin, UpdateView): 'app': _('Assets'), 'action': _('Update command filter rule'), 'object': self.cmd_filter, + 'rule': self.get_object(), + 'request_type': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) \ No newline at end of file From 63a502ba62f9252dda5c97a9c3e47e8bbcef61fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:54:30 +0800 Subject: [PATCH 10/19] =?UTF-8?q?[Update]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E7=94=A8=E6=88=B7=20=E4=BD=BF=E7=94=A8api=20(#2918?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 创建/更新 用户 使用api * [Update] 修改小问题 * [Update] 修改小问题 --- apps/users/serializers/v1.py | 14 ++- apps/users/templates/users/user_create.html | 30 +++-- apps/users/templates/users/user_update.html | 118 +++++++++++--------- 3 files changed, 100 insertions(+), 62 deletions(-) diff --git a/apps/users/serializers/v1.py b/apps/users/serializers/v1.py index 63fe52699..864618823 100644 --- a/apps/users/serializers/v1.py +++ b/apps/users/serializers/v1.py @@ -36,7 +36,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): 'date_password_last_updated', 'date_expired', 'avatar_url', ] extra_kwargs = { - 'password': {'write_only': True, 'required': False}, + 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'public_key': {'write_only': True}, 'groups_display': {'label': _('Groups name')}, 'source_display': {'label': _('Source name')}, @@ -56,13 +56,17 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): raise serializers.ValidationError(msg) return value - @staticmethod - def validate_password(value): + def validate_password(self, password): from ..utils import check_password_rules - if not check_password_rules(value): + password_strategy = self.initial_data.get('password_strategy') + if password_strategy == '0': + return + if password_strategy is None and not password: + return + if not check_password_rules(password): msg = _('Password does not match security rules') raise serializers.ValidationError(msg) - return value + return password @staticmethod def change_password_to_raw(validated_data): diff --git a/apps/users/templates/users/user_create.html b/apps/users/templates/users/user_create.html index 13bb26cbc..5e15b5469 100644 --- a/apps/users/templates/users/user_create.html +++ b/apps/users/templates/users/user_create.html @@ -4,9 +4,7 @@ {% block user_template_title %}{% trans "Create user" %}{% endblock %} {% block password %} {% bootstrap_field form.password_strategy layout="horizontal" %} -
- {% bootstrap_field form.password layout="horizontal" %} -
+ {% bootstrap_field form.password layout="horizontal" %} {# 密码popover #}
- +
- - - - - + + + + + - {% for asset in object_list %} - - - - - - {% endfor %}
{% trans 'Hostname' %}{% trans 'IP' %}
+ + {% trans 'Hostname' %}{% trans 'IP' %}
{{ asset.hostname }}{{ asset.ip }} - -
-
- {% include '_pagination.html' %} -
@@ -86,9 +76,6 @@ @@ -146,6 +133,7 @@ +{% include 'assets/_asset_list_modal.html' %} {% endblock %} {% block custom_foot_js %} - {% endblock %} \ No newline at end of file diff --git a/apps/assets/utils.py b/apps/assets/utils.py index a0de3b481..be8a80351 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -1,7 +1,8 @@ # ~*~ coding: utf-8 ~*~ # import time -from django.db.models import Prefetch +from functools import reduce +from django.db.models import Prefetch, Q from common.utils import get_object_or_none, get_logger from common.struct import Stack @@ -21,24 +22,34 @@ def get_system_user_by_id(id): return system_user -class LabelFilter: - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - query_keys = self.request.query_params.keys() +class LabelFilterMixin: + def get_filter_labels_ids(self): + query_params = self.request.query_params + query_keys = query_params.keys() all_label_keys = Label.objects.values_list('name', flat=True) valid_keys = set(all_label_keys) & set(query_keys) - labels_query = {} - for key in valid_keys: - labels_query[key] = self.request.query_params.get(key) - conditions = [] - for k, v in labels_query.items(): - query = {'labels__name': k, 'labels__value': v} - conditions.append(query) + if not valid_keys: + return [] - if conditions: - for kwargs in conditions: - queryset = queryset.filter(**kwargs) + labels_query = [ + {"name": key, "value": query_params[key]} + for key in valid_keys + ] + args = [Q(**kwargs) for kwargs in labels_query] + args = reduce(lambda x, y: x | y, args) + labels_id = Label.objects.filter(args).values_list('id', flat=True) + return labels_id + + +class LabelFilter(LabelFilterMixin): + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + labels_ids = self.get_filter_labels_ids() + if not labels_ids: + return queryset + for labels_id in labels_ids: + queryset = queryset.filter(labels=labels_id) return queryset diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index ce701b129..40601d27f 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -69,7 +69,7 @@ class UserAssetListView(PermissionsMixin, TemplateView): context = { 'action': _('My assets'), 'labels': Label.objects.all().order_by('name'), - 'system_users': SystemUser.objects.all(), + 'show_actions': True } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/authentication/templates/authentication/_mfa_confirm_modal.html b/apps/authentication/templates/authentication/_mfa_confirm_modal.html index 0d7b794bb..60512d7de 100644 --- a/apps/authentication/templates/authentication/_mfa_confirm_modal.html +++ b/apps/authentication/templates/authentication/_mfa_confirm_modal.html @@ -38,7 +38,7 @@ $(document).ready(function () { var error = function () { $("#mfa_error").addClass("text-danger").html(codeError); }; - APIUpdateAttr({ + requestApi({ url: url, method: "POST", body: JSON.stringify(data), diff --git a/apps/common/permissions.py b/apps/common/permissions.py index edb5ee4d0..bdc25fe21 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -145,13 +145,13 @@ class NeedMFAVerify(permissions.BasePermission): return False -class CanUpdateSuperUser(permissions.BasePermission): +class CanUpdateDeleteSuperUser(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in ['GET', 'OPTIONS']: return True - if str(request.user.id) == str(obj.id): + elif request.method == 'DELETE' and str(request.user.id) == str(obj.id): return False - if request.user.is_superuser: + elif request.user.is_superuser: return True if hasattr(obj, 'is_superuser') and obj.is_superuser: return False diff --git a/apps/ops/templates/ops/command_execution_create.html b/apps/ops/templates/ops/command_execution_create.html index 3e4436e41..17d2c044a 100644 --- a/apps/ops/templates/ops/command_execution_create.html +++ b/apps/ops/templates/ops/command_execution_create.html @@ -255,7 +255,7 @@ function execute() { } } - APIUpdateAttr({ + requestApi({ url: url, body: JSON.stringify(data), method: 'POST', diff --git a/apps/ops/templates/ops/task_list.html b/apps/ops/templates/ops/task_list.html index 0426e059d..ae8c45043 100644 --- a/apps/ops/templates/ops/task_list.html +++ b/apps/ops/templates/ops/task_list.html @@ -109,7 +109,7 @@ $(document).ready(function() { var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); window.open(url, '', 'width=800,height=600,left=400,top=400') }; - APIUpdateAttr({ + requestApi({ url: the_url, error: error, method: 'GET', diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py new file mode 100644 index 000000000..24bd9abd2 --- /dev/null +++ b/apps/perms/api/mixin.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# +from functools import reduce +from hashlib import md5 +from django.core.cache import cache +from django.db.models import Q +from django.conf import settings +from rest_framework.views import Response + +from django.utils.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, Label +from .. import serializers + +logger = get_logger(__name__) + +__all__ = ['UserPermissionCacheMixin', 'GrantAssetsMixin', 'NodesWithUngroupMixin'] + + +class UserPermissionCacheMixin: + cache_policy = '0' + RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_V2_{}' + 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)) + + def get(self, request, *args, **kwargs): + self.cache_policy = request.GET.get('cache_policy', '0') + + obj = self._get_object() + if obj is None: + logger.debug("Not get response from cache: obj is none") + return super().get(request, *args, **kwargs) + + if AssetPermissionUtil.is_not_using_cache(self.cache_policy): + logger.debug("Not get resp from cache: {}".format(self.cache_policy)) + return super().get(request, *args, **kwargs) + elif AssetPermissionUtil.is_refresh_cache(self.cache_policy): + logger.debug("Not get resp from cache: {}".format(self.cache_policy)) + self.expire_response_cache() + + logger.debug("Try get response from cache") + resp = self.get_response_from_cache() + if not resp: + resp = super().get(request, *args, **kwargs) + self.set_response_to_cache(resp) + return resp + + +class NodesWithUngroupMixin: + util = None + + @staticmethod + def get_ungrouped_node(ungroup_key): + return Node(key=ungroup_key, id=const.UNGROUPED_NODE_ID, + value=_("ungrouped")) + + @staticmethod + def get_empty_node(): + return Node(key=const.EMPTY_NODE_KEY, id=const.EMPTY_NODE_ID, + value=_("empty")) + + def add_ungrouped_nodes(self, node_map, node_keys): + ungroup_key = '1:-1' + for key in node_keys: + if key.endswith('-1'): + ungroup_key = key + break + ungroup_node = self.get_ungrouped_node(ungroup_key) + empty_node = self.get_empty_node() + node_map[ungroup_key] = ungroup_node + node_map[const.EMPTY_NODE_KEY] = empty_node + + +class GrantAssetsMixin(LabelFilterMixin): + serializer_class = serializers.AssetGrantedSerializer + + def get_serializer_queryset(self, queryset): + assets_ids = [] + system_users_ids = set() + for asset in queryset: + assets_ids.append(asset["id"]) + system_users_ids.update(set(asset["system_users"])) + assets = Asset.objects.filter(id__in=assets_ids).only( + *self.serializer_class.Meta.only_fields + ) + assets_map = {asset.id: asset for asset in assets} + system_users = SystemUser.objects.filter(id__in=system_users_ids).only( + *self.serializer_class.system_users_only_fields + ) + system_users_map = {s.id: s for s in system_users} + data = [] + for item in queryset: + i = item["id"] + asset = assets_map.get(i) + if not asset: + continue + + _system_users = item["system_users"] + system_users_granted = [] + for sid, action in _system_users.items(): + system_user = system_users_map.get(sid) + if not system_user: + continue + system_user.actions = action + system_users_granted.append(system_user) + asset.system_users_granted = system_users_granted + 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 search_queryset(self, assets_items): + search = self.request.query_params.get("search") + if not search: + return assets_items + assets_map = {asset['id']: asset for asset in assets_items} + assets_ids = set(assets_map.keys()) + assets_ids_search = Asset.objects.filter(id__in=assets_ids).filter( + Q(hostname__icontains=search) | Q(ip__icontains=search) + ).values_list('id', flat=True) + return [assets_map.get(asset_id) for asset_id in assets_ids_search] + + def filter_queryset_by_label(self, assets_items): + labels_id = self.get_filter_labels_ids() + if not labels_id: + return assets_items + + assets_map = {asset['id']: asset for asset in assets_items} + assets_matched = Asset.objects.filter(id__in=assets_map.keys()) + for label_id in labels_id: + assets_matched = assets_matched.filter(labels=label_id) + assets_ids_matched = assets_matched.values_list('id', flat=True) + return [assets_map.get(asset_id) for asset_id in assets_ids_matched] + + def sort_queryset(self, assets_items): + order_by = self.request.query_params.get('order', 'hostname') + + if order_by not in ['hostname', '-hostname', 'ip', '-ip']: + order_by = 'hostname' + assets_map = {asset['id']: asset for asset in assets_items} + assets_ids_search = Asset.objects.filter(id__in=assets_map.keys())\ + .order_by(order_by)\ + .values_list('id', flat=True) + return [assets_map.get(asset_id) for asset_id in assets_ids_search] + + def filter_queryset(self, assets_items): + assets_items = self.search_queryset(assets_items) + assets_items = self.filter_queryset_by_label(assets_items) + assets_items = self.sort_queryset(assets_items) + return assets_items \ No newline at end of file diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py index 9e32055d4..f83330c58 100644 --- a/apps/perms/api/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -2,23 +2,21 @@ # from django.shortcuts import get_object_or_404 -from rest_framework.generics import ( - ListAPIView, get_object_or_404, -) from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from ..hands import UserGroup -from .. import serializers, const +from .. import serializers from .user_permission import ( UserGrantedAssetsApi, UserGrantedNodesApi, UserGrantedNodesWithAssetsApi, UserGrantedNodesWithAssetsAsTreeApi, UserGrantedNodeAssetsApi, + UserGrantedNodesAsTreeApi, ) __all__ = [ 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi', - 'UserGroupGrantedNodesWithAssetsAsTreeApi', + 'UserGroupGrantedNodesWithAssetsAsTreeApi', 'UserGroupGrantedNodesAsTreeApi', ] @@ -36,6 +34,13 @@ class UserGroupGrantedNodesApi(UserGrantedNodesApi): return user_group +class UserGroupGrantedNodesAsTreeApi(UserGrantedNodesAsTreeApi): + def get_object(self): + user_group_id = self.kwargs.get('pk', '') + user_group = get_object_or_404(UserGroup, id=user_group_id) + return user_group + + class UserGroupGrantedNodesWithAssetsApi(UserGrantedNodesWithAssetsApi): permission_classes = (IsOrgAdmin,) serializer_class = serializers.NodeGrantedSerializer diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py index 8d55ee49c..0e67e2747 100644 --- a/apps/perms/api/user_permission.py +++ b/apps/perms/api/user_permission.py @@ -2,25 +2,23 @@ # import time import traceback +from functools import reduce import uuid -from hashlib import md5 -from django.core.cache import cache -from django.conf import settings from django.db.models import Q from django.shortcuts import get_object_or_404 from rest_framework.views import APIView, Response from rest_framework.generics import ( ListAPIView, get_object_or_404, RetrieveAPIView ) -from django.utils.translation import ugettext as _ from rest_framework.pagination import LimitOffsetPagination from common.permissions import IsValidUser, IsOrgAdminOrAppUser from common.tree import TreeNodeSerializer -from common.utils import get_logger, get_object_or_none +from common.utils import get_logger from ..utils import ( AssetPermissionUtil, ParserNode, ) +from .mixin import UserPermissionCacheMixin, GrantAssetsMixin, NodesWithUngroupMixin from .. import const from ..hands import User, Asset, Node, SystemUser, NodeSerializer from .. import serializers @@ -37,153 +35,6 @@ __all__ = [ ] -class UserPermissionCacheMixin: - cache_policy = '0' - RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_V2_{}' - 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)) - - def get(self, request, *args, **kwargs): - 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 GrantAssetsMixin: - serializer_class = serializers.AssetGrantedSerializer - - def get_serializer(self, queryset, many=True): - assets_ids = [] - system_users_ids = set() - for asset in queryset: - assets_ids.append(asset["id"]) - system_users_ids.update(set(asset["system_users"])) - assets = Asset.objects.filter(id__in=assets_ids).only( - *self.serializer_class.Meta.only_fields - ) - assets_map = {asset.id: asset for asset in assets} - system_users = SystemUser.objects.filter(id__in=system_users_ids).only( - *self.serializer_class.system_users_only_fields - ) - system_users_map = {s.id: s for s in system_users} - data = [] - for item in queryset: - i = item["id"] - asset = assets_map.get(i) - if not asset: - continue - - _system_users = item["system_users"] - system_users_granted = [] - for sid, action in _system_users.items(): - system_user = system_users_map.get(sid) - if not system_user: - continue - system_user.actions = action - system_users_granted.append(system_user) - asset.system_users_granted = system_users_granted - data.append(asset) - return super().get_serializer(data, many=True) - - def search_queryset(self, assets): - search = self.request.query_params.get("search") - if not search: - return assets - - assets_map = {asset['id']: asset for asset in assets} - assets_ids = set(assets_map.keys()) - assets_ids_search = Asset.objects.filter(id__in=assets_ids).filter( - Q(hostname__icontains=search) | Q(ip__icontains=search) - ).values_list('id', flat=True) - assets_ids &= set(assets_ids_search) - return [assets_map.get(asset_id) for asset_id in assets_ids] - - class UserGrantedAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIView): """ 用户授权的所有资产 @@ -203,7 +54,6 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIVi user = self.get_object() util = AssetPermissionUtil(user, cache_policy=self.cache_policy) queryset = util.get_assets() - queryset = self.search_queryset(queryset) return queryset def get_permissions(self): @@ -212,29 +62,52 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIVi return super().get_permissions() -class NodesWithUngroupMixin: - util = None +class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIView): + """ + 查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产 + """ + permission_classes = (IsOrgAdminOrAppUser,) + pagination_class = LimitOffsetPagination - @staticmethod - def get_ungrouped_node(ungroup_key): - return Node(key=ungroup_key, id=const.UNGROUPED_NODE_ID, - value=_("ungrouped")) + def get_object(self): + user_id = self.kwargs.get('pk', '') - @staticmethod - def get_empty_node(): - return Node(key=const.EMPTY_NODE_KEY, id=const.EMPTY_NODE_ID, - value=_("empty")) + if user_id: + user = get_object_or_404(User, id=user_id) + else: + user = self.request.user + return user - def add_ungrouped_nodes(self, node_map, node_keys): - ungroup_key = '1:-1' - for key in node_keys: - if key.endswith('-1'): - ungroup_key = key + def get_node_key(self): + node_id = self.kwargs.get('node_id') + if str(node_id) == const.UNGROUPED_NODE_ID: + key = self.util.tree.ungrouped_key + elif str(node_id) == const.EMPTY_NODE_ID: + key = const.EMPTY_NODE_KEY + else: + node = get_object_or_404(Node, id=node_id) + key = node.key + return key + + def get_queryset(self): + user = self.get_object() + self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy) + key = self.get_node_key() + nodes_items = self.util.get_nodes_with_assets() + assets_system_users = {} + for item in nodes_items: + if item["key"] == key: + assets_system_users = item["assets"] break - ungroup_node = self.get_ungrouped_node(ungroup_key) - empty_node = self.get_empty_node() - node_map[ungroup_key] = ungroup_node - node_map[const.EMPTY_NODE_KEY] = empty_node + assets = [] + for asset_id, system_users in assets_system_users.items(): + assets.append({"id": asset_id, "system_users": system_users}) + return assets + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() class UserGrantedNodesApi(UserPermissionCacheMixin, NodesWithUngroupMixin, ListAPIView): @@ -435,55 +308,6 @@ class UserGrantedNodesWithAssetsAsTreeApi(UserGrantedNodesWithAssetsApi): return self.serializer_class(queryset, many=True) -class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, GrantAssetsMixin, ListAPIView): - """ - 查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产 - """ - permission_classes = (IsOrgAdminOrAppUser,) - pagination_class = LimitOffsetPagination - - def get_object(self): - user_id = self.kwargs.get('pk', '') - - if user_id: - user = get_object_or_404(User, id=user_id) - else: - user = self.request.user - return user - - def get_node_key(self): - node_id = self.kwargs.get('node_id') - if str(node_id) == const.UNGROUPED_NODE_ID: - key = self.util.tree.ungrouped_key - elif str(node_id) == const.EMPTY_NODE_ID: - key = const.EMPTY_NODE_KEY - else: - node = get_object_or_404(Node, id=node_id) - key = node.key - return key - - def get_queryset(self): - user = self.get_object() - self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy) - key = self.get_node_key() - nodes_items = self.util.get_nodes_with_assets() - assets_system_users = {} - for item in nodes_items: - if item["key"] == key: - assets_system_users = item["assets"] - break - assets = [] - for asset_id, system_users in assets_system_users.items(): - assets.append({"id": asset_id, "system_users": system_users}) - assets = self.search_queryset(assets) - return assets - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView): permission_classes = (IsOrgAdminOrAppUser,) @@ -522,16 +346,12 @@ class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, RetrieveAPIView system_id = self.request.query_params.get('system_user_id', '') user = get_object_or_404(User, id=user_id) - asset = get_object_or_404(Asset, id=asset_id) - su = get_object_or_404(SystemUser, id=system_id) util = AssetPermissionUtil(user, cache_policy=self.cache_policy) - granted_assets = util.get_assets() - granted_system_users = granted_assets.get(asset, {}) - - _object = {} - if su not in granted_system_users: - _object['actions'] = 0 - else: - _object['actions'] = granted_system_users[su] - return _object + assets = util.get_assets() + actions = 0 + for asset in assets: + if asset_id == asset["id"]: + actions = asset["system_users"].get(system_id, 0) + break + return {"actions": actions} diff --git a/apps/perms/hands.py b/apps/perms/hands.py index bbdc01e1e..aef0f4875 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -2,7 +2,7 @@ # from users.models import User, UserGroup -from assets.models import Asset, SystemUser, Node +from assets.models import Asset, SystemUser, Node, Label from assets.serializers import NodeSerializer from applications.serializers import RemoteAppSerializer from applications.models import RemoteApp diff --git a/apps/perms/templates/perms/asset_permission_asset.html b/apps/perms/templates/perms/asset_permission_asset.html index ea5b20e63..d8cd8ee96 100644 --- a/apps/perms/templates/perms/asset_permission_asset.html +++ b/apps/perms/templates/perms/asset_permission_asset.html @@ -145,7 +145,7 @@ function addAssets(assets) { var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -160,7 +160,7 @@ function removeAssets(assets) { var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -172,7 +172,7 @@ function updateNodes(nodes, success) { var body = { nodes: nodes }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success diff --git a/apps/perms/templates/perms/asset_permission_detail.html b/apps/perms/templates/perms/asset_permission_detail.html index 9c38cfc29..f9c11cb67 100644 --- a/apps/perms/templates/perms/asset_permission_detail.html +++ b/apps/perms/templates/perms/asset_permission_detail.html @@ -187,7 +187,7 @@ function updateSystemUser(system_users) { var body = { system_users: Object.assign([], system_users) }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body) }); @@ -247,7 +247,7 @@ $(document).ready(function () { var body = { 'is_active': checked }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), }); diff --git a/apps/perms/templates/perms/asset_permission_user.html b/apps/perms/templates/perms/asset_permission_user.html index eef6bf601..d5e8292e1 100644 --- a/apps/perms/templates/perms/asset_permission_user.html +++ b/apps/perms/templates/perms/asset_permission_user.html @@ -160,7 +160,7 @@ function addUsers(users) { var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -175,7 +175,7 @@ function removeUser(users) { var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -187,7 +187,7 @@ function updateGroup(groups) { var body = { user_groups: groups }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body) }); diff --git a/apps/perms/templates/perms/remote_app_permission_detail.html b/apps/perms/templates/perms/remote_app_permission_detail.html index 98bc6633e..352e9f17a 100644 --- a/apps/perms/templates/perms/remote_app_permission_detail.html +++ b/apps/perms/templates/perms/remote_app_permission_detail.html @@ -160,7 +160,7 @@ $(document).ready(function () { var body = { 'is_active': checked }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body) }); diff --git a/apps/perms/templates/perms/remote_app_permission_remote_app.html b/apps/perms/templates/perms/remote_app_permission_remote_app.html index 63d395941..2e705945f 100644 --- a/apps/perms/templates/perms/remote_app_permission_remote_app.html +++ b/apps/perms/templates/perms/remote_app_permission_remote_app.html @@ -120,7 +120,7 @@ var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -134,7 +134,7 @@ var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success diff --git a/apps/perms/templates/perms/remote_app_permission_user.html b/apps/perms/templates/perms/remote_app_permission_user.html index 7433327c5..b1fc8e488 100644 --- a/apps/perms/templates/perms/remote_app_permission_user.html +++ b/apps/perms/templates/perms/remote_app_permission_user.html @@ -158,7 +158,7 @@ var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -172,7 +172,7 @@ var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -183,7 +183,7 @@ var body = { user_groups: groups }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body) }); diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py index 7075bda8e..093692a15 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -39,9 +39,10 @@ asset_permission_urlpatterns = [ # 查询某个用户组授权的资产和资产组 path('user-groups//assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), - path('user-groups//nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), - path('user-groups//nodes-assets/', api.UserGroupGrantedNodesWithAssetsApi.as_view(), name='user-group-nodes-assets'), + path('user-groups//nodes/tree/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), + path('user-groups//nodes/', api.UserGroupGrantedNodesAsTreeApi.as_view(), name='user-group-nodes-as-tree'), path('user-groups//nodes-assets/tree/', api.UserGroupGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-group-nodes-assets-as-tree'), + path('user-groups//nodes-assets/', api.UserGroupGrantedNodesWithAssetsApi.as_view(), name='user-group-nodes-assets'), path('user-groups//nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), # 用户和资产授权变更 diff --git a/apps/settings/templates/settings/email_setting.html b/apps/settings/templates/settings/email_setting.html index 46c4f5dac..40ce9f4cc 100644 --- a/apps/settings/templates/settings/email_setting.html +++ b/apps/settings/templates/settings/email_setting.html @@ -96,7 +96,7 @@ $(document).ready(function () { function success(message) { toastr.success(message.msg) } - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(data), method: "POST", diff --git a/apps/settings/templates/settings/ldap_setting.html b/apps/settings/templates/settings/ldap_setting.html index 58e4ae71b..694fb66f9 100644 --- a/apps/settings/templates/settings/ldap_setting.html +++ b/apps/settings/templates/settings/ldap_setting.html @@ -100,7 +100,7 @@ $(document).ready(function () { function success(message) { toastr.success(message.msg) } - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(data), method: "POST", @@ -127,7 +127,7 @@ $(document).ready(function () { function success(message) { toastr.success(message.msg) } - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify({'username_list':username_list}), method: "POST", diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index a216a1ce0..c880a5499 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -256,7 +256,7 @@ function formSubmit(props) { }) } -function APIUpdateAttr(props) { +function requestApi(props) { // props = {url: .., body: , success: , error: , method: ,} props = props || {}; var user_success_message = props.success_message; @@ -328,7 +328,7 @@ function objectDelete(obj, name, url, redirectTo) { // swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error"); swal(gettext('Error'), "[ "+name+" ]" + gettext("Being used by the asset, please unbind the asset first."), "error"); }; - APIUpdateAttr({ + requestApi({ url: url, body: JSON.stringify(body), method: 'DELETE', @@ -369,7 +369,7 @@ function orgDelete(obj, name, url, redirectTo){ swal(gettext("Error"), " [ "+ name + " ] " + gettext("Do not perform this operation under this organization. Try again after switching to another organization"), "error"); } }; - APIUpdateAttr({ + requestApi({ url: url, body: JSON.stringify(body), method: 'DELETE', @@ -1109,7 +1109,19 @@ function objectAttrsIsBool(obj, attrs) { }) } +function cleanDate(d) { + for (var i=0; i<2; i++) { + if (isNaN(Date.parse(d))) { + d = d.split('+')[0].trimRight(); + } else { + return d + } + } + return '' +} + function formatDateAsCN(d) { + d = cleanDate(d); var date = new Date(d); var date_s = date.toLocaleString(navigator.language, {hour12: false}); return date_s.split("/").join('-') @@ -1138,6 +1150,8 @@ function getTimeUnits(u) { } function timeOffset(a, b) { + a = cleanDate(a); + b = cleanDate(b); var start = new Date(a); var end = new Date(b); var offset = (end - start)/1000; diff --git a/apps/static/js/plugins/moment/moment.min.js b/apps/static/js/plugins/moment/moment.min.js new file mode 100644 index 000000000..8e6866af0 --- /dev/null +++ b/apps/static/js/plugins/moment/moment.min.js @@ -0,0 +1,7 @@ +//! moment.js +//! version : 2.10.6 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return Hc.apply(null,arguments)}function b(a){Hc=a}function c(a){return"[object Array]"===Object.prototype.toString.call(a)}function d(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function e(a,b){var c,d=[];for(c=0;c0)for(c in Jc)d=Jc[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function n(b){m(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),Kc===!1&&(Kc=!0,a.updateOffset(this),Kc=!1)}function o(a){return a instanceof n||null!=a&&null!=a._isAMomentObject}function p(a){return 0>a?Math.ceil(a):Math.floor(a)}function q(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=p(b)),c}function r(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++;return g+f}function s(){}function t(a){return a?a.toLowerCase().replace("_","-"):a}function u(a){for(var b,c,d,e,f=0;f0;){if(d=v(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&r(e,c,!0)>=b-1)break;b--}f++}return null}function v(a){var b=null;if(!Lc[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=Ic._abbr,require("./locale/"+a),w(b)}catch(c){}return Lc[a]}function w(a,b){var c;return a&&(c="undefined"==typeof b?y(a):x(a,b),c&&(Ic=c)),Ic._abbr}function x(a,b){return null!==b?(b.abbr=a,Lc[a]=Lc[a]||new s,Lc[a].set(b),w(a),Lc[a]):(delete Lc[a],null)}function y(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return Ic;if(!c(a)){if(b=v(a))return b;a=[a]}return u(a)}function z(a,b){var c=a.toLowerCase();Mc[c]=Mc[c+"s"]=Mc[b]=a}function A(a){return"string"==typeof a?Mc[a]||Mc[a.toLowerCase()]:void 0}function B(a){var b,c,d={};for(c in a)f(a,c)&&(b=A(c),b&&(d[b]=a[c]));return d}function C(b,c){return function(d){return null!=d?(E(this,b,d),a.updateOffset(this,c),this):D(this,b)}}function D(a,b){return a._d["get"+(a._isUTC?"UTC":"")+b]()}function E(a,b,c){return a._d["set"+(a._isUTC?"UTC":"")+b](c)}function F(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=A(a),"function"==typeof this[a])return this[a](b);return this}function G(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function H(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Qc[a]=e),b&&(Qc[b[0]]=function(){return G(e.apply(this,arguments),b[1],b[2])}),c&&(Qc[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function I(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function J(a){var b,c,d=a.match(Nc);for(b=0,c=d.length;c>b;b++)Qc[d[b]]?d[b]=Qc[d[b]]:d[b]=I(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function K(a,b){return a.isValid()?(b=L(b,a.localeData()),Pc[b]=Pc[b]||J(b),Pc[b](a)):a.localeData().invalidDate()}function L(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Oc.lastIndex=0;d>=0&&Oc.test(a);)a=a.replace(Oc,c),Oc.lastIndex=0,d-=1;return a}function M(a){return"function"==typeof a&&"[object Function]"===Object.prototype.toString.call(a)}function N(a,b,c){dd[a]=M(b)?b:function(a){return a&&c?c:b}}function O(a,b){return f(dd,a)?dd[a](b._strict,b._locale):new RegExp(P(a))}function P(a){return a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Q(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=q(a)}),c=0;cd;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function X(a,b){var c;return"string"==typeof b&&(b=a.localeData().monthsParse(b),"number"!=typeof b)?a:(c=Math.min(a.date(),T(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a)}function Y(b){return null!=b?(X(this,b),a.updateOffset(this,!0),this):D(this,"Month")}function Z(){return T(this.year(),this.month())}function $(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[gd]<0||c[gd]>11?gd:c[hd]<1||c[hd]>T(c[fd],c[gd])?hd:c[id]<0||c[id]>24||24===c[id]&&(0!==c[jd]||0!==c[kd]||0!==c[ld])?id:c[jd]<0||c[jd]>59?jd:c[kd]<0||c[kd]>59?kd:c[ld]<0||c[ld]>999?ld:-1,j(a)._overflowDayOfYear&&(fd>b||b>hd)&&(b=hd),j(a).overflow=b),a}function _(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function aa(a,b){var c=!0;return g(function(){return c&&(_(a+"\n"+(new Error).stack),c=!1),b.apply(this,arguments)},b)}function ba(a,b){od[a]||(_(b),od[a]=!0)}function ca(a){var b,c,d=a._i,e=pd.exec(d);if(e){for(j(a).iso=!0,b=0,c=qd.length;c>b;b++)if(qd[b][1].exec(d)){a._f=qd[b][0];break}for(b=0,c=rd.length;c>b;b++)if(rd[b][1].exec(d)){a._f+=(e[6]||" ")+rd[b][0];break}d.match(ad)&&(a._f+="Z"),va(a)}else a._isValid=!1}function da(b){var c=sd.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(ca(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function ea(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function fa(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function ga(a){return ha(a)?366:365}function ha(a){return a%4===0&&a%100!==0||a%400===0}function ia(){return ha(this.year())}function ja(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=Da(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function ka(a){return ja(a,this._week.dow,this._week.doy).week}function la(){return this._week.dow}function ma(){return this._week.doy}function na(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function oa(a){var b=ja(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function pa(a,b,c,d,e){var f,g=6+e-d,h=fa(a,0,1+g),i=h.getUTCDay();return e>i&&(i+=7),c=null!=c?1*c:e,f=1+g+7*(b-1)-i+c,{year:f>0?a:a-1,dayOfYear:f>0?f:ga(a-1)+f}}function qa(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function ra(a,b,c){return null!=a?a:null!=b?b:c}function sa(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function ta(a){var b,c,d,e,f=[];if(!a._d){for(d=sa(a),a._w&&null==a._a[hd]&&null==a._a[gd]&&ua(a),a._dayOfYear&&(e=ra(a._a[fd],d[fd]),a._dayOfYear>ga(e)&&(j(a)._overflowDayOfYear=!0),c=fa(e,0,a._dayOfYear),a._a[gd]=c.getUTCMonth(),a._a[hd]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[id]&&0===a._a[jd]&&0===a._a[kd]&&0===a._a[ld]&&(a._nextDay=!0,a._a[id]=0),a._d=(a._useUTC?fa:ea).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[id]=24)}}function ua(a){var b,c,d,e,f,g,h;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=ra(b.GG,a._a[fd],ja(Da(),1,4).year),d=ra(b.W,1),e=ra(b.E,1)):(f=a._locale._week.dow,g=a._locale._week.doy,c=ra(b.gg,a._a[fd],ja(Da(),f,g).year),d=ra(b.w,1),null!=b.d?(e=b.d,f>e&&++d):e=null!=b.e?b.e+f:f),h=pa(c,d,e,g,f),a._a[fd]=h.year,a._dayOfYear=h.dayOfYear}function va(b){if(b._f===a.ISO_8601)return void ca(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=L(b._f,b._locale).match(Nc)||[],c=0;c0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),Qc[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),S(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[id]<=12&&b._a[id]>0&&(j(b).bigHour=void 0),b._a[id]=wa(b._locale,b._a[id],b._meridiem),ta(b),$(b)}function wa(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function xa(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function ya(a){if(!a._d){var b=B(a._i);a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],ta(a)}}function za(a){var b=new n($(Aa(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function Aa(a){var b=a._i,e=a._f;return a._locale=a._locale||y(a._l),null===b||void 0===e&&""===b?l({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),o(b)?new n($(b)):(c(e)?xa(a):e?va(a):d(b)?a._d=b:Ba(a),a))}function Ba(b){var f=b._i;void 0===f?b._d=new Date:d(f)?b._d=new Date(+f):"string"==typeof f?da(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),ta(b)):"object"==typeof f?ya(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function Ca(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,za(f)}function Da(a,b,c,d){return Ca(a,b,c,d,!1)}function Ea(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Da();for(d=b[0],e=1;ea&&(a=-a,c="-"),c+G(~~(a/60),2)+b+G(~~a%60,2)})}function Ka(a){var b=(a||"").match(ad)||[],c=b[b.length-1]||[],d=(c+"").match(xd)||["-",0,0],e=+(60*d[1])+q(d[2]);return"+"===d[0]?e:-e}function La(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(o(b)||d(b)?+b:+Da(b))-+e,e._d.setTime(+e._d+f),a.updateOffset(e,!1),e):Da(b).local()}function Ma(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Na(b,c){var d,e=this._offset||0;return null!=b?("string"==typeof b&&(b=Ka(b)),Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ma(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?bb(this,Ya(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ma(this)}function Oa(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Pa(a){return this.utcOffset(0,a)}function Qa(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ma(this),"m")),this}function Ra(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ka(this._i)),this}function Sa(a){return a=a?Da(a).utcOffset():0,(this.utcOffset()-a)%60===0}function Ta(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ua(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var a={};if(m(a,this),a=Aa(a),a._a){var b=a._isUTC?h(a._a):Da(a._a);this._isDSTShifted=this.isValid()&&r(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Va(){return!this._isUTC}function Wa(){return this._isUTC}function Xa(){return this._isUTC&&0===this._offset}function Ya(a,b){var c,d,e,g=a,h=null;return Ia(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=yd.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:q(h[hd])*c,h:q(h[id])*c,m:q(h[jd])*c,s:q(h[kd])*c,ms:q(h[ld])*c}):(h=zd.exec(a))?(c="-"===h[1]?-1:1,g={y:Za(h[2],c),M:Za(h[3],c),d:Za(h[4],c),h:Za(h[5],c),m:Za(h[6],c),s:Za(h[7],c),w:Za(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=_a(Da(g.from),Da(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Ha(g),Ia(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function Za(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function $a(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function _a(a,b){var c;return b=La(b,a),a.isBefore(b)?c=$a(a,b):(c=$a(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c}function ab(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(ba(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ya(c,d),bb(this,e,a),this}}function bb(b,c,d,e){var f=c._milliseconds,g=c._days,h=c._months;e=null==e?!0:e,f&&b._d.setTime(+b._d+f*d),g&&E(b,"Date",D(b,"Date")+g*d),h&&X(b,D(b,"Month")+h*d),e&&a.updateOffset(b,g||h)}function cb(a,b){var c=a||Da(),d=La(c,this).startOf("day"),e=this.diff(d,"days",!0),f=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(b&&b[f]||this.localeData().calendar(f,this,Da(c)))}function db(){return new n(this)}function eb(a,b){var c;return b=A("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=o(a)?a:Da(a),+this>+a):(c=o(a)?+a:+Da(a),c<+this.clone().startOf(b))}function fb(a,b){var c;return b=A("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=o(a)?a:Da(a),+a>+this):(c=o(a)?+a:+Da(a),+this.clone().endOf(b)b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function kb(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function lb(){var a=this.clone().utc();return 0b;b++)if(this._weekdaysParse[b]||(c=Da([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b}function Pb(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Kb(a,this.localeData()),this.add(a-b,"d")):b}function Qb(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Rb(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)}function Sb(a,b){H(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function Tb(a,b){return b._meridiemParse}function Ub(a){return"p"===(a+"").toLowerCase().charAt(0)}function Vb(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Wb(a,b){b[ld]=q(1e3*("0."+a))}function Xb(){return this._isUTC?"UTC":""}function Yb(){return this._isUTC?"Coordinated Universal Time":""}function Zb(a){return Da(1e3*a)}function $b(){return Da.apply(null,arguments).parseZone()}function _b(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.call(b,c):d}function ac(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function bc(){return this._invalidDate}function cc(a){return this._ordinal.replace("%d",a)}function dc(a){return a}function ec(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)}function fc(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)}function gc(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function hc(a,b,c,d){var e=y(),f=h().set(d,b);return e[c](f,a)}function ic(a,b,c,d,e){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return hc(a,b,c,e);var f,g=[];for(f=0;d>f;f++)g[f]=hc(a,f,c,e);return g}function jc(a,b){return ic(a,b,"months",12,"month")}function kc(a,b){return ic(a,b,"monthsShort",12,"month")}function lc(a,b){return ic(a,b,"weekdays",7,"day")}function mc(a,b){return ic(a,b,"weekdaysShort",7,"day")}function nc(a,b){return ic(a,b,"weekdaysMin",7,"day")}function oc(){var a=this._data;return this._milliseconds=Wd(this._milliseconds),this._days=Wd(this._days),this._months=Wd(this._months),a.milliseconds=Wd(a.milliseconds),a.seconds=Wd(a.seconds),a.minutes=Wd(a.minutes),a.hours=Wd(a.hours),a.months=Wd(a.months),a.years=Wd(a.years),this}function pc(a,b,c,d){var e=Ya(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function qc(a,b){return pc(this,a,b,1)}function rc(a,b){return pc(this,a,b,-1)}function sc(a){return 0>a?Math.floor(a):Math.ceil(a)}function tc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*sc(vc(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=p(f/1e3),i.seconds=a%60,b=p(a/60),i.minutes=b%60,c=p(b/60),i.hours=c%24,g+=p(c/24),e=p(uc(g)),h+=e,g-=sc(vc(e)),d=p(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function uc(a){return 4800*a/146097}function vc(a){return 146097*a/4800}function wc(a){var b,c,d=this._milliseconds;if(a=A(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+uc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(vc(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function xc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*q(this._months/12)}function yc(a){return function(){return this.as(a)}}function zc(a){return a=A(a),this[a+"s"]()}function Ac(a){return function(){return this._data[a]}}function Bc(){return p(this.days()/7)}function Cc(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function Dc(a,b,c){var d=Ya(a).abs(),e=ke(d.as("s")),f=ke(d.as("m")),g=ke(d.as("h")),h=ke(d.as("d")),i=ke(d.as("M")),j=ke(d.as("y")),k=e0,k[4]=c,Cc.apply(null,k)}function Ec(a,b){return void 0===le[a]?!1:void 0===b?le[a]:(le[a]=b,!0)}function Fc(a){var b=this.localeData(),c=Dc(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function Gc(){var a,b,c,d=me(this._milliseconds)/1e3,e=me(this._days),f=me(this._months);a=p(d/60),b=p(a/60),d%=60,a%=60,c=p(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var Hc,Ic,Jc=a.momentProperties=[],Kc=!1,Lc={},Mc={},Nc=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Oc=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Pc={},Qc={},Rc=/\d/,Sc=/\d\d/,Tc=/\d{3}/,Uc=/\d{4}/,Vc=/[+-]?\d{6}/,Wc=/\d\d?/,Xc=/\d{1,3}/,Yc=/\d{1,4}/,Zc=/[+-]?\d{1,6}/,$c=/\d+/,_c=/[+-]?\d+/,ad=/Z|[+-]\d\d:?\d\d/gi,bd=/[+-]?\d+(\.\d{1,3})?/,cd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,dd={},ed={},fd=0,gd=1,hd=2,id=3,jd=4,kd=5,ld=6;H("M",["MM",2],"Mo",function(){return this.month()+1}),H("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),H("MMMM",0,0,function(a){return this.localeData().months(this,a)}),z("month","M"),N("M",Wc),N("MM",Wc,Sc),N("MMM",cd),N("MMMM",cd),Q(["M","MM"],function(a,b){b[gd]=q(a)-1}),Q(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);null!=e?b[gd]=e:j(c).invalidMonth=a});var md="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),nd="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),od={};a.suppressDeprecationWarnings=!1;var pd=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,qd=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],rd=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],sd=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=aa("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),H(0,["YY",2],0,function(){return this.year()%100}),H(0,["YYYY",4],0,"year"),H(0,["YYYYY",5],0,"year"),H(0,["YYYYYY",6,!0],0,"year"),z("year","y"),N("Y",_c),N("YY",Wc,Sc),N("YYYY",Yc,Uc),N("YYYYY",Zc,Vc),N("YYYYYY",Zc,Vc),Q(["YYYYY","YYYYYY"],fd),Q("YYYY",function(b,c){c[fd]=2===b.length?a.parseTwoDigitYear(b):q(b)}),Q("YY",function(b,c){c[fd]=a.parseTwoDigitYear(b)}),a.parseTwoDigitYear=function(a){return q(a)+(q(a)>68?1900:2e3)};var td=C("FullYear",!1);H("w",["ww",2],"wo","week"),H("W",["WW",2],"Wo","isoWeek"),z("week","w"),z("isoWeek","W"),N("w",Wc),N("ww",Wc,Sc),N("W",Wc),N("WW",Wc,Sc),R(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=q(a)});var ud={dow:0,doy:6};H("DDD",["DDDD",3],"DDDo","dayOfYear"),z("dayOfYear","DDD"),N("DDD",Xc),N("DDDD",Tc),Q(["DDD","DDDD"],function(a,b,c){c._dayOfYear=q(a)}),a.ISO_8601=function(){};var vd=aa("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var a=Da.apply(null,arguments);return this>a?this:a}),wd=aa("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var a=Da.apply(null,arguments);return a>this?this:a});Ja("Z",":"),Ja("ZZ",""),N("Z",ad),N("ZZ",ad),Q(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ka(a)});var xd=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var yd=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,zd=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Ya.fn=Ha.prototype;var Ad=ab(1,"add"),Bd=ab(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var Cd=aa("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});H(0,["gg",2],0,function(){return this.weekYear()%100}),H(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Db("gggg","weekYear"),Db("ggggg","weekYear"),Db("GGGG","isoWeekYear"),Db("GGGGG","isoWeekYear"),z("weekYear","gg"),z("isoWeekYear","GG"),N("G",_c),N("g",_c),N("GG",Wc,Sc),N("gg",Wc,Sc),N("GGGG",Yc,Uc),N("gggg",Yc,Uc),N("GGGGG",Zc,Vc),N("ggggg",Zc,Vc),R(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=q(a)}),R(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),H("Q",0,0,"quarter"),z("quarter","Q"),N("Q",Rc),Q("Q",function(a,b){b[gd]=3*(q(a)-1)}),H("D",["DD",2],"Do","date"),z("date","D"),N("D",Wc),N("DD",Wc,Sc),N("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),Q(["D","DD"],hd),Q("Do",function(a,b){b[hd]=q(a.match(Wc)[0],10)});var Dd=C("Date",!0);H("d",0,"do","day"),H("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),H("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),H("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),H("e",0,0,"weekday"),H("E",0,0,"isoWeekday"),z("day","d"),z("weekday","e"),z("isoWeekday","E"),N("d",Wc),N("e",Wc),N("E",Wc),N("dd",cd),N("ddd",cd),N("dddd",cd),R(["dd","ddd","dddd"],function(a,b,c){var d=c._locale.weekdaysParse(a);null!=d?b.d=d:j(c).invalidWeekday=a}),R(["d","e","E"],function(a,b,c,d){b[d]=q(a)});var Ed="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Fd="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Gd="Su_Mo_Tu_We_Th_Fr_Sa".split("_");H("H",["HH",2],0,"hour"),H("h",["hh",2],0,function(){return this.hours()%12||12}),Sb("a",!0),Sb("A",!1),z("hour","h"),N("a",Tb),N("A",Tb),N("H",Wc),N("h",Wc),N("HH",Wc,Sc),N("hh",Wc,Sc),Q(["H","HH"],id),Q(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),Q(["h","hh"],function(a,b,c){b[id]=q(a),j(c).bigHour=!0});var Hd=/[ap]\.?m?\.?/i,Id=C("Hours",!0);H("m",["mm",2],0,"minute"),z("minute","m"),N("m",Wc),N("mm",Wc,Sc),Q(["m","mm"],jd);var Jd=C("Minutes",!1);H("s",["ss",2],0,"second"),z("second","s"),N("s",Wc),N("ss",Wc,Sc),Q(["s","ss"],kd);var Kd=C("Seconds",!1);H("S",0,0,function(){return~~(this.millisecond()/100)}),H(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),H(0,["SSS",3],0,"millisecond"),H(0,["SSSS",4],0,function(){return 10*this.millisecond()}),H(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),H(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),H(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),H(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),H(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),z("millisecond","ms"),N("S",Xc,Rc),N("SS",Xc,Sc),N("SSS",Xc,Tc);var Ld;for(Ld="SSSS";Ld.length<=9;Ld+="S")N(Ld,$c);for(Ld="S";Ld.length<=9;Ld+="S")Q(Ld,Wb);var Md=C("Milliseconds",!1);H("z",0,0,"zoneAbbr"),H("zz",0,0,"zoneName");var Nd=n.prototype;Nd.add=Ad,Nd.calendar=cb,Nd.clone=db,Nd.diff=ib,Nd.endOf=ub,Nd.format=mb,Nd.from=nb,Nd.fromNow=ob,Nd.to=pb,Nd.toNow=qb,Nd.get=F,Nd.invalidAt=Cb,Nd.isAfter=eb,Nd.isBefore=fb,Nd.isBetween=gb,Nd.isSame=hb,Nd.isValid=Ab,Nd.lang=Cd,Nd.locale=rb,Nd.localeData=sb,Nd.max=wd,Nd.min=vd,Nd.parsingFlags=Bb,Nd.set=F,Nd.startOf=tb,Nd.subtract=Bd,Nd.toArray=yb,Nd.toObject=zb,Nd.toDate=xb,Nd.toISOString=lb,Nd.toJSON=lb,Nd.toString=kb,Nd.unix=wb,Nd.valueOf=vb,Nd.year=td,Nd.isLeapYear=ia,Nd.weekYear=Fb,Nd.isoWeekYear=Gb,Nd.quarter=Nd.quarters=Jb,Nd.month=Y,Nd.daysInMonth=Z,Nd.week=Nd.weeks=na,Nd.isoWeek=Nd.isoWeeks=oa,Nd.weeksInYear=Ib,Nd.isoWeeksInYear=Hb,Nd.date=Dd,Nd.day=Nd.days=Pb,Nd.weekday=Qb,Nd.isoWeekday=Rb,Nd.dayOfYear=qa,Nd.hour=Nd.hours=Id,Nd.minute=Nd.minutes=Jd,Nd.second=Nd.seconds=Kd, +Nd.millisecond=Nd.milliseconds=Md,Nd.utcOffset=Na,Nd.utc=Pa,Nd.local=Qa,Nd.parseZone=Ra,Nd.hasAlignedHourOffset=Sa,Nd.isDST=Ta,Nd.isDSTShifted=Ua,Nd.isLocal=Va,Nd.isUtcOffset=Wa,Nd.isUtc=Xa,Nd.isUTC=Xa,Nd.zoneAbbr=Xb,Nd.zoneName=Yb,Nd.dates=aa("dates accessor is deprecated. Use date instead.",Dd),Nd.months=aa("months accessor is deprecated. Use month instead",Y),Nd.years=aa("years accessor is deprecated. Use year instead",td),Nd.zone=aa("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Oa);var Od=Nd,Pd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Qd={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Rd="Invalid date",Sd="%d",Td=/\d{1,2}/,Ud={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Vd=s.prototype;Vd._calendar=Pd,Vd.calendar=_b,Vd._longDateFormat=Qd,Vd.longDateFormat=ac,Vd._invalidDate=Rd,Vd.invalidDate=bc,Vd._ordinal=Sd,Vd.ordinal=cc,Vd._ordinalParse=Td,Vd.preparse=dc,Vd.postformat=dc,Vd._relativeTime=Ud,Vd.relativeTime=ec,Vd.pastFuture=fc,Vd.set=gc,Vd.months=U,Vd._months=md,Vd.monthsShort=V,Vd._monthsShort=nd,Vd.monthsParse=W,Vd.week=ka,Vd._week=ud,Vd.firstDayOfYear=ma,Vd.firstDayOfWeek=la,Vd.weekdays=Lb,Vd._weekdays=Ed,Vd.weekdaysMin=Nb,Vd._weekdaysMin=Gd,Vd.weekdaysShort=Mb,Vd._weekdaysShort=Fd,Vd.weekdaysParse=Ob,Vd.isPM=Ub,Vd._meridiemParse=Hd,Vd.meridiem=Vb,w("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===q(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=aa("moment.lang is deprecated. Use moment.locale instead.",w),a.langData=aa("moment.langData is deprecated. Use moment.localeData instead.",y);var Wd=Math.abs,Xd=yc("ms"),Yd=yc("s"),Zd=yc("m"),$d=yc("h"),_d=yc("d"),ae=yc("w"),be=yc("M"),ce=yc("y"),de=Ac("milliseconds"),ee=Ac("seconds"),fe=Ac("minutes"),ge=Ac("hours"),he=Ac("days"),ie=Ac("months"),je=Ac("years"),ke=Math.round,le={s:45,m:45,h:22,d:26,M:11},me=Math.abs,ne=Ha.prototype;ne.abs=oc,ne.add=qc,ne.subtract=rc,ne.as=wc,ne.asMilliseconds=Xd,ne.asSeconds=Yd,ne.asMinutes=Zd,ne.asHours=$d,ne.asDays=_d,ne.asWeeks=ae,ne.asMonths=be,ne.asYears=ce,ne.valueOf=xc,ne._bubble=tc,ne.get=zc,ne.milliseconds=de,ne.seconds=ee,ne.minutes=fe,ne.hours=ge,ne.days=he,ne.weeks=Bc,ne.months=ie,ne.years=je,ne.humanize=Fc,ne.toISOString=Gc,ne.toString=Gc,ne.toJSON=Gc,ne.locale=rb,ne.localeData=sb,ne.toIsoString=aa("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Gc),ne.lang=Cd,H("X",0,0,"unix"),H("x",0,0,"valueOf"),N("x",_c),N("X",bd),Q("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),Q("x",function(a,b,c){c._d=new Date(q(a))}),a.version="2.10.6",b(Da),a.fn=Od,a.min=Fa,a.max=Ga,a.utc=h,a.unix=Zb,a.months=jc,a.isDate=d,a.locale=w,a.invalid=l,a.duration=Ya,a.isMoment=o,a.weekdays=lc,a.parseZone=$b,a.localeData=y,a.isDuration=Ia,a.monthsShort=kc,a.weekdaysMin=nc,a.defineLocale=x,a.weekdaysShort=mc,a.normalizeUnits=A,a.relativeTimeThreshold=Ec;var oe=a;return oe}); \ No newline at end of file diff --git a/apps/terminal/templates/terminal/session_detail.html b/apps/terminal/templates/terminal/session_detail.html index 8bec6c50c..b54eedec7 100644 --- a/apps/terminal/templates/terminal/session_detail.html +++ b/apps/terminal/templates/terminal/session_detail.html @@ -132,7 +132,7 @@ }, 300) } var the_url = "{% url 'api-terminal:tasks-list' %}"; - APIUpdateAttr({url: the_url, method: 'POST', body: JSON.stringify(data), success: success, success_message: 'Terminate success'}); + requestApi({url: the_url, method: 'POST', body: JSON.stringify(data), success: success, success_message: 'Terminate success'}); } $(document).ready(function () { $('.footable').footable(); diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index e9a992481..5dd2e24df 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -90,7 +90,7 @@ function terminateSession(data) { } var success_message = '{% trans "Terminate task send, waiting ..." %}'; var the_url = "{% url 'api-terminal:kill-session' %}"; - APIUpdateAttr({ + requestApi({ url: the_url, method: 'POST', body: JSON.stringify(data), @@ -174,7 +174,7 @@ function finishedSession(data) { var success = function() { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, method: 'PATCH', body: JSON.stringify(data), diff --git a/apps/users/api/user.py b/apps/users/api/user.py index c79b9e865..ba8e48b25 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -14,7 +14,7 @@ from rest_framework.pagination import LimitOffsetPagination from common.permissions import ( IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser, - CanUpdateSuperUser, + CanUpdateDeleteSuperUser, ) from common.mixins import IDInCacheFilterMixin from common.utils import get_logger @@ -38,7 +38,7 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): search_fields = filter_fields queryset = User.objects.exclude(role=User.ROLE_APP) serializer_class = UserSerializer - permission_classes = (IsOrgAdmin, CanUpdateSuperUser) + permission_classes = (IsOrgAdmin, CanUpdateDeleteSuperUser) pagination_class = LimitOffsetPagination def send_created_signal(self, users): diff --git a/apps/users/templates/users/_granted_assets.html b/apps/users/templates/users/_granted_assets.html new file mode 100644 index 000000000..36cc27a1c --- /dev/null +++ b/apps/users/templates/users/_granted_assets.html @@ -0,0 +1,165 @@ +{% load i18n %} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + + + + + + + {% if show_actions %} + + {% endif %} + + + + +
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'System user' %}{% trans 'Action' %}
+
+
+ diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 351823a9e..929d7def4 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -280,7 +280,7 @@ function updateUserGroups(groups) { // clear jumpserver.groups_selected jumpserver.nodes_selected = {}; }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -305,7 +305,7 @@ $(document).ready(function() { 'is_active': checked }; var success = '{% trans "Update successfully!" %}'; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success_message: success @@ -332,7 +332,7 @@ $(document).ready(function() { 'otp_secret_key': otp_secret_key }; var success = '{% trans "Update successfully!" %}'; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success_message: success @@ -372,7 +372,7 @@ $(document).ready(function() { var msg = "{% trans "An e-mail has been sent to the user`s mailbox." %}"; swal("{% trans 'Reset password' %}", msg, "success"); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -398,7 +398,7 @@ $(document).ready(function() { var msg = "{% trans 'The reset-ssh-public-key E-mail has been sent successfully. Please inform the user to update his new ssh public key.' %}"; swal("{% trans 'Reset SSH public key' %}", msg, "success"); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: body, success: success @@ -441,7 +441,7 @@ $(document).ready(function() { } ); }; - APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail}); + requestApi({ url: the_url, body: JSON.stringify(body), success: success, error: fail}); }).on('click', '.btn-delete-user', function () { var $this = $(this); var name = "{{ user_object.name }}"; @@ -466,7 +466,7 @@ $(document).ready(function() { } ); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -485,7 +485,7 @@ $(document).ready(function() { doReset(); }); }).on('click', '#btn-reset-mfa', function () { - APIUpdateAttr({ + requestApi({ url: "{% url 'api-users:user-reset-otp' pk=user_object.id %}", method: "GET", success_message: "{% trans 'Reset user MFA success' %}" diff --git a/apps/users/templates/users/user_granted_asset.html b/apps/users/templates/users/user_granted_asset.html index 225005ffd..fc5197a77 100644 --- a/apps/users/templates/users/user_granted_asset.html +++ b/apps/users/templates/users/user_granted_asset.html @@ -23,33 +23,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'System users' %}
-
-
+ {% include 'users/_granted_assets.html' %}
@@ -58,81 +32,10 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} \ No newline at end of file diff --git a/apps/perms/views/remote_app_permission.py b/apps/perms/views/remote_app_permission.py index 3e8a6bff4..91774be0d 100644 --- a/apps/perms/views/remote_app_permission.py +++ b/apps/perms/views/remote_app_permission.py @@ -48,6 +48,7 @@ class RemoteAppPermissionCreateView(PermissionsMixin, CreateView): context = { 'app': _('Perms'), 'action': _('Create RemoteApp permission'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -63,7 +64,8 @@ class RemoteAppPermissionUpdateView(PermissionsMixin, UpdateView): def get_context_data(self, **kwargs): context = { 'app': _('Perms'), - 'action': _('Update RemoteApp permission') + 'action': _('Update RemoteApp permission'), + 'type': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) From 4ba306f5976619e8c5ba4b11418b4e23763e4c5b Mon Sep 17 00:00:00 2001 From: BaiJiangJie Date: Mon, 15 Jul 2019 14:14:23 +0800 Subject: [PATCH 15/19] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E6=94=B9=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E8=A7=84=E5=88=99=E8=AF=A6=E6=83=85=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=BB=84=E6=95=B0=E9=87=8F=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/templates/perms/asset_permission_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/perms/templates/perms/asset_permission_detail.html b/apps/perms/templates/perms/asset_permission_detail.html index f9c11cb67..beaa5a362 100644 --- a/apps/perms/templates/perms/asset_permission_detail.html +++ b/apps/perms/templates/perms/asset_permission_detail.html @@ -68,7 +68,7 @@ {% trans 'User group count' %}: - {{ object.users.count }} + {{ object.user_groups.count }} {% trans 'Asset count' %}: From ab9744d52928475481c58629514d6f3d16329146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 15 Jul 2019 14:36:11 +0800 Subject: [PATCH 16/19] Bugfix (#2944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改assets * [Update] 修改nodes --- apps/assets/forms/asset.py | 6 +++--- apps/perms/forms/asset_permission.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 5973f731b..a044988ab 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -29,9 +29,9 @@ class ProtocolForm(forms.Form): class AssetCreateForm(OrgModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if not self.data: - nodes_field = self.fields['nodes'] - nodes_field._queryset = Node.get_queryset() + nodes_field = self.fields['nodes'] + nodes_field.choices = ((n.id, n.full_value) for n in + Node.get_queryset()) class Meta: model = Asset diff --git a/apps/perms/forms/asset_permission.py b/apps/perms/forms/asset_permission.py index d0b362a1b..da3096b30 100644 --- a/apps/perms/forms/asset_permission.py +++ b/apps/perms/forms/asset_permission.py @@ -41,6 +41,9 @@ class AssetPermissionForm(OrgModelForm): users_field = self.fields.get('users') users_field.queryset = current_org.get_org_users() + nodes_field = self.fields['nodes'] + nodes_field.choices = ((n.id, n.full_value) for n in Node.get_queryset()) + # 前端渲染优化, 防止过多资产 if not self.data: instance = kwargs.get('instance') @@ -49,8 +52,6 @@ class AssetPermissionForm(OrgModelForm): assets_field.queryset = instance.assets.all() else: assets_field.queryset = Asset.objects.none() - nodes_field = self.fields['nodes'] - nodes_field._queryset = Node.get_queryset() class Meta: model = AssetPermission From 9219786f2dbb6e661bc60357a85541fd4c55e867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Mon, 15 Jul 2019 14:42:54 +0800 Subject: [PATCH 17/19] =?UTF-8?q?[Update]=20=E5=88=9B=E5=BB=BA/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E8=BF=9C=E7=A8=8B=E5=BA=94=E7=94=A8=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8api=20(#2940)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 创建/更新 远程应用 使用api * [Update] 优化 params * [Update] 修改小问题 --- .../remote_app_create_update.html | 36 ++++++++++++++++++- apps/applications/views/remote_app.py | 2 ++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/applications/templates/applications/remote_app_create_update.html b/apps/applications/templates/applications/remote_app_create_update.html index ecd7254a1..fe6b231da 100644 --- a/apps/applications/templates/applications/remote_app_create_update.html +++ b/apps/applications/templates/applications/remote_app_create_update.html @@ -107,6 +107,18 @@ function hiddenFields(){ }); $('.' + app_type + '-fields').removeClass('hidden'); } +function constructParams(obj) { + var type = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']; + var params = {}; + type.forEach(function (attr) { + if (obj.type === attr){ + for (var k in obj){ + if (k.startsWith(obj.type)){params[k] = obj[k]} + } + } + }); + return params +} $(document).ready(function () { $('.select2').select2({ closeOnSelect: true @@ -118,6 +130,28 @@ $(document).ready(function () { .on('change', app_type_id, function(){ hiddenFields(); setDefaultValue(); -}); +}) +.on("submit", "form", function (evt) { + evt.preventDefault(); + var the_url = '{% url "api-applications:remote-app-list" %}'; + var redirect_to = '{% url "applications:remote-app-list" %}'; + var method = "POST"; + {% if type == "update" %} + the_url = '{% url "api-applications:remote-app-detail" object.id %}'; + method = "PUT"; + {% endif %} + var form = $("form"); + var data = form.serializeObject(); + data["params"] = constructParams(data); + var props = { + url: the_url, + data: data, + method: method, + form: form, + redirect_to: redirect_to + }; + formSubmit(props); + }) +; {% endblock %} \ No newline at end of file diff --git a/apps/applications/views/remote_app.py b/apps/applications/views/remote_app.py index 5576ed3bb..e7f6f0ccd 100644 --- a/apps/applications/views/remote_app.py +++ b/apps/applications/views/remote_app.py @@ -46,6 +46,7 @@ class RemoteAppCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): context = { 'app': _('Applications'), 'action': _('Create RemoteApp'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -68,6 +69,7 @@ class RemoteAppUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): context = { 'app': _('Applications'), 'action': _('Update RemoteApp'), + 'type': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) From 92aeecbc3e0edb58338300221c909a44e5a87c17 Mon Sep 17 00:00:00 2001 From: BaiJiangJie Date: Mon, 15 Jul 2019 14:57:57 +0800 Subject: [PATCH 18/19] =?UTF-8?q?[Update]=20=E4=BC=98=E5=8C=96=E5=88=9B?= =?UTF-8?q?=E5=BB=BARemoteApp=E5=89=8D=E7=AB=AF=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote_app_create_update.html | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/applications/templates/applications/remote_app_create_update.html b/apps/applications/templates/applications/remote_app_create_update.html index fe6b231da..897126f98 100644 --- a/apps/applications/templates/applications/remote_app_create_update.html +++ b/apps/applications/templates/applications/remote_app_create_update.html @@ -107,17 +107,20 @@ function hiddenFields(){ }); $('.' + app_type + '-fields').removeClass('hidden'); } -function constructParams(obj) { - var type = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']; +function constructParams(data) { + var typeList = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']; var params = {}; - type.forEach(function (attr) { - if (obj.type === attr){ - for (var k in obj){ - if (k.startsWith(obj.type)){params[k] = obj[k]} + for (var type in typeList){ + if (data.type === type){ + for (var k in data){ + if (k.startsWith(data.type)){ + params[k] = data[k] + } } + break } - }); - return params + } + return params; } $(document).ready(function () { $('.select2').select2({ From 9d54baac09ec004b438a98bc27511a0bcd81ca90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 15 Jul 2019 15:11:01 +0800 Subject: [PATCH 19/19] Secure (#2946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改assets * [Update] 添加关闭terminal注册的设置 * [Update] 去掉sql打印 --- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 77406 -> 77648 bytes apps/locale/zh/LC_MESSAGES/django.po | 394 +++++++++++------------ apps/settings/forms.py | 5 + apps/terminal/serializers_v2/terminal.py | 4 + 6 files changed, 207 insertions(+), 199 deletions(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 17d660be8..ce43c13b1 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -359,6 +359,7 @@ defaults = { 'TERMINAL_TELNET_REGEX': '', 'TERMINAL_COMMAND_STORAGE': {}, 'SECURITY_MFA_AUTH': False, + 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, 'SECURITY_LOGIN_LIMIT_COUNT': 7, 'SECURITY_LOGIN_LIMIT_TIME': 30, 'SECURITY_MAX_IDLE_TIME': 30, diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 35ffebc25..6ee9f7a74 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -568,7 +568,7 @@ SECURITY_PASSWORD_RULES = [ 'SECURITY_PASSWORD_SPECIAL_CHAR' ] SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL - +SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index c702b74b11afc1edab7a3ebae020e5bf22ca9dd3..9dc31d0c8a0ac28755cc69794b4aa36137412452 100644 GIT binary patch delta 23088 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 delta 22853 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 PkR%}N2mY\n" "Language-Team: Jumpserver team\n" @@ -76,8 +76,8 @@ msgstr "运行参数" #: applications/templates/applications/remote_app_list.html:22 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:342 assets/models/authbook.py:24 -#: assets/serializers/admin_user.py:35 assets/serializers/asset_user.py:81 +#: assets/models/asset.py:319 assets/models/authbook.py:24 +#: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:81 #: assets/serializers/system_user.py:30 #: assets/templates/assets/admin_user_list.html:47 #: assets/templates/assets/domain_detail.html:60 @@ -86,7 +86,7 @@ msgstr "运行参数" #: assets/templates/assets/system_user_list.html:55 audits/models.py:19 #: audits/templates/audits/ftp_log_list.html:41 #: audits/templates/audits/ftp_log_list.html:71 -#: perms/forms/asset_permission.py:68 perms/models/asset_permission.py:76 +#: perms/forms/asset_permission.py:69 perms/models/asset_permission.py:78 #: perms/templates/perms/asset_permission_create_update.html:45 #: perms/templates/perms/asset_permission_list.html:48 #: perms/templates/perms/asset_permission_list.html:117 @@ -112,11 +112,11 @@ msgstr "资产" #: applications/templates/applications/remote_app_detail.html:61 #: applications/templates/applications/remote_app_list.html:23 #: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/user.py:160 assets/templates/assets/user_asset_list.html:172 +#: assets/models/user.py:150 assets/templates/assets/user_asset_list.html:52 #: audits/models.py:20 audits/templates/audits/ftp_log_list.html:49 #: audits/templates/audits/ftp_log_list.html:72 -#: perms/forms/asset_permission.py:74 perms/models/asset_permission.py:78 -#: perms/models/asset_permission.py:103 +#: perms/forms/asset_permission.py:75 perms/models/asset_permission.py:80 +#: perms/models/asset_permission.py:114 #: perms/templates/perms/asset_permission_detail.html:140 #: perms/templates/perms/asset_permission_list.html:50 #: perms/templates/perms/asset_permission_list.html:71 @@ -126,6 +126,7 @@ msgstr "资产" #: terminal/templates/terminal/command_list.html:67 #: terminal/templates/terminal/session_list.html:29 #: terminal/templates/terminal/session_list.html:73 +#: users/templates/users/_granted_assets.html:26 #: xpack/plugins/orgs/templates/orgs/org_list.html:19 msgid "System user" msgstr "系统用户" @@ -166,7 +167,7 @@ msgstr "系统用户" #: settings/templates/settings/terminal_setting.html:105 terminal/models.py:22 #: terminal/models.py:258 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:64 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:324 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 @@ -205,7 +206,7 @@ msgstr "参数" #: applications/models/remote_app.py:43 #: applications/templates/applications/remote_app_detail.html:77 -#: assets/models/asset.py:221 assets/models/base.py:36 +#: assets/models/asset.py:198 assets/models/base.py:36 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 #: assets/models/cmd_filter.py:58 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:68 @@ -214,10 +215,10 @@ msgstr "参数" #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 #: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:14 -#: perms/models/asset_permission.py:106 perms/models/base.py:41 +#: 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:105 users/serializers/v1.py:116 +#: users/models/user.py:365 users/serializers/v1.py:120 #: 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 @@ -229,7 +230,7 @@ msgstr "创建者" # msgstr "创建者" #: applications/models/remote_app.py:46 #: applications/templates/applications/remote_app_detail.html:73 -#: assets/models/asset.py:222 assets/models/base.py:34 +#: assets/models/asset.py:199 assets/models/base.py:34 #: assets/models/cluster.py:26 assets/models/domain.py:23 #: assets/models/group.py:22 assets/models/label.py:25 #: assets/templates/assets/admin_user_detail.html:64 @@ -237,7 +238,7 @@ msgstr "创建者" #: assets/templates/assets/domain_detail.html:68 #: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 -#: orgs/models.py:15 perms/models/asset_permission.py:107 +#: orgs/models.py:15 perms/models/asset_permission.py:118 #: perms/models/base.py:42 #: perms/templates/perms/asset_permission_detail.html:94 #: perms/templates/perms/remote_app_permission_detail.html:86 @@ -257,7 +258,7 @@ msgstr "创建日期" #: applications/templates/applications/remote_app_detail.html:81 #: applications/templates/applications/remote_app_list.html:24 #: applications/templates/applications/user_remote_app_list.html:20 -#: assets/models/asset.py:223 assets/models/base.py:33 +#: assets/models/asset.py:200 assets/models/base.py:33 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 #: assets/models/cmd_filter.py:55 assets/models/domain.py:21 #: assets/models/domain.py:53 assets/models/group.py:23 @@ -271,15 +272,14 @@ msgstr "创建日期" #: assets/templates/assets/domain_gateway_list.html:72 #: assets/templates/assets/domain_list.html:28 #: assets/templates/assets/system_user_detail.html:104 -#: assets/templates/assets/system_user_list.html:59 -#: assets/templates/assets/user_asset_list.html:175 ops/models/adhoc.py:43 -#: orgs/models.py:16 perms/models/asset_permission.py:108 +#: assets/templates/assets/system_user_list.html:59 ops/models/adhoc.py:43 +#: orgs/models.py:16 perms/models/asset_permission.py:119 #: perms/models/base.py:43 #: perms/templates/perms/asset_permission_detail.html:102 #: perms/templates/perms/remote_app_permission_detail.html:94 #: settings/models.py:34 terminal/models.py:32 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:97 users/templates/users/user_detail.html:127 +#: users/models/user.py:357 users/templates/users/user_detail.html:127 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:134 @@ -522,8 +522,7 @@ msgstr "创建远程应用" #: assets/templates/assets/domain_gateway_list.html:73 #: assets/templates/assets/domain_list.html:29 #: assets/templates/assets/label_list.html:17 -#: assets/templates/assets/system_user_list.html:60 -#: assets/templates/assets/user_asset_list.html:48 audits/models.py:38 +#: 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 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 @@ -537,6 +536,7 @@ msgstr "创建远程应用" #: settings/templates/settings/terminal_setting.html:107 #: terminal/templates/terminal/session_list.html:36 #: terminal/templates/terminal/terminal_list.html:36 +#: users/templates/users/_granted_assets.html:28 #: users/templates/users/user_group_list.html:38 #: users/templates/users/user_list.html:41 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:60 @@ -549,8 +549,8 @@ msgid "Action" msgstr "动作" #: applications/templates/applications/user_remote_app_list.html:57 -#: assets/templates/assets/user_asset_list.html:100 -#: perms/models/asset_permission.py:28 +#: assets/templates/assets/user_asset_list.html:31 +#: perms/models/asset_permission.py:30 msgid "Connect" msgstr "连接" @@ -603,7 +603,6 @@ msgstr "不可达" #: assets/const.py:78 assets/models/utils.py:44 #: assets/templates/assets/asset_list.html:99 -#: users/templates/users/user_group_granted_asset.html:47 msgid "Reachable" msgstr "可连接" @@ -612,24 +611,23 @@ msgstr "可连接" msgid "Unknown" msgstr "未知" -#: assets/forms/asset.py:24 assets/models/asset.py:187 +#: assets/forms/asset.py:24 assets/models/asset.py:164 #: assets/models/domain.py:50 #: assets/templates/assets/domain_gateway_list.html:69 -#: assets/templates/assets/user_asset_list.html:168 #: settings/templates/settings/replay_storage_create.html:59 msgid "Port" msgstr "端口" -#: assets/forms/asset.py:45 assets/models/asset.py:192 +#: assets/forms/asset.py:45 assets/models/asset.py:169 #: assets/models/user.py:107 assets/templates/assets/asset_detail.html:190 #: assets/templates/assets/asset_detail.html:198 #: assets/templates/assets/system_user_assets.html:83 -#: perms/models/asset_permission.py:77 +#: perms/models/asset_permission.py:79 #: xpack/plugins/change_auth_plan/models.py:72 msgid "Nodes" msgstr "节点" -#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:196 +#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:173 #: assets/models/cluster.py:19 assets/models/user.py:65 #: assets/templates/assets/asset_detail.html:76 templates/_nav.html:24 #: xpack/plugins/cloud/models.py:124 @@ -642,24 +640,24 @@ msgstr "管理用户" #: assets/templates/assets/asset_create.html:48 #: assets/templates/assets/asset_create.html:50 #: assets/templates/assets/asset_list.html:85 -#: assets/templates/assets/user_asset_list.html:33 +#: users/templates/users/_granted_assets.html:16 #: xpack/plugins/orgs/templates/orgs/org_list.html:20 msgid "Label" msgstr "标签" -#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:191 +#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:168 #: assets/models/domain.py:26 assets/models/domain.py:52 #: assets/templates/assets/asset_detail.html:80 -#: assets/templates/assets/user_asset_list.html:173 +#: assets/templates/assets/user_asset_list.html:53 #: xpack/plugins/orgs/templates/orgs/org_list.html:17 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:254 +#: assets/forms/asset.py:128 assets/models/node.py:253 #: assets/templates/assets/asset_create.html:42 -#: perms/forms/asset_permission.py:71 perms/forms/asset_permission.py:78 -#: perms/models/asset_permission.py:101 +#: perms/forms/asset_permission.py:72 perms/forms/asset_permission.py:79 +#: perms/models/asset_permission.py:112 #: perms/templates/perms/asset_permission_list.html:49 #: perms/templates/perms/asset_permission_list.html:70 #: perms/templates/perms/asset_permission_list.html:120 @@ -691,13 +689,13 @@ msgstr "如果有多个的互相隔离的网络,设置资产属于的网域, #: assets/forms/asset.py:108 assets/forms/asset.py:112 #: assets/forms/domain.py:17 assets/forms/label.py:15 -#: perms/templates/perms/asset_permission_asset.html:88 +#: perms/templates/perms/asset_permission_asset.html:78 #: xpack/plugins/change_auth_plan/forms.py:106 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:84 msgid "Select assets" msgstr "选择资产" -#: assets/forms/cmd_filter.py:37 assets/serializers/cmd_filter.py:34 +#: assets/forms/cmd_filter.py:37 assets/serializers/cmd_filter.py:40 msgid "Content should not be contain: {}" msgstr "内容不能包含: {}" @@ -726,7 +724,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 #: settings/templates/settings/_ldap_list_users_modal.html:37 users/forms.py:14 -#: users/models/user.py:62 users/templates/users/_select_user_modal.html:14 +#: users/models/user.py:322 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 @@ -765,7 +763,7 @@ msgstr "密码" #: assets/forms/user.py:29 assets/serializers/asset_user.py:70 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:91 +#: users/models/user.py:351 msgid "Private key" msgstr "ssh私钥" @@ -805,134 +803,129 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写" msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:182 assets/models/domain.py:49 +#: assets/models/asset.py:159 assets/models/domain.py:49 #: assets/serializers/asset_user.py:28 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/_asset_user_list.html:15 #: assets/templates/assets/asset_detail.html:64 #: assets/templates/assets/asset_list.html:97 #: assets/templates/assets/domain_gateway_list.html:68 -#: assets/templates/assets/user_asset_list.html:45 -#: assets/templates/assets/user_asset_list.html:167 +#: assets/templates/assets/user_asset_list.html:49 #: audits/templates/audits/login_log_list.html:54 -#: perms/templates/perms/asset_permission_asset.html:55 settings/forms.py:140 -#: users/templates/users/user_granted_asset.html:45 -#: users/templates/users/user_group_granted_asset.html:45 +#: perms/templates/perms/asset_permission_asset.html:58 settings/forms.py:140 +#: users/templates/users/_granted_assets.html:25 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:51 msgid "IP" msgstr "IP" -#: assets/models/asset.py:183 assets/serializers/asset_user.py:27 +#: assets/models/asset.py:160 assets/serializers/asset_user.py:27 #: assets/templates/assets/_asset_list_modal.html:45 #: assets/templates/assets/_asset_user_auth_update_modal.html:9 #: assets/templates/assets/_asset_user_auth_view_modal.html:15 #: assets/templates/assets/_asset_user_list.html:14 #: assets/templates/assets/asset_detail.html:60 #: assets/templates/assets/asset_list.html:96 -#: assets/templates/assets/user_asset_list.html:44 -#: assets/templates/assets/user_asset_list.html:166 -#: perms/templates/perms/asset_permission_asset.html:54 +#: assets/templates/assets/user_asset_list.html:48 +#: perms/templates/perms/asset_permission_asset.html:57 #: perms/templates/perms/asset_permission_list.html:69 settings/forms.py:139 -#: users/templates/users/user_granted_asset.html:44 -#: users/templates/users/user_group_granted_asset.html:44 +#: users/templates/users/_granted_assets.html:24 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:50 msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:186 assets/models/domain.py:51 +#: assets/models/asset.py:163 assets/models/domain.py:51 #: assets/models/user.py:110 assets/templates/assets/asset_detail.html:72 #: assets/templates/assets/domain_gateway_list.html:70 #: assets/templates/assets/system_user_detail.html:70 #: assets/templates/assets/system_user_list.html:53 -#: assets/templates/assets/user_asset_list.html:169 #: terminal/templates/terminal/session_list.html:31 msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:189 assets/serializers/asset.py:63 +#: assets/models/asset.py:166 assets/serializers/asset.py:63 #: assets/templates/assets/asset_create.html:24 +#: assets/templates/assets/user_asset_list.html:50 +#: perms/serializers/user_permission.py:38 msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:190 assets/templates/assets/asset_detail.html:104 -#: assets/templates/assets/user_asset_list.html:170 +#: assets/models/asset.py:167 assets/templates/assets/asset_detail.html:104 +#: assets/templates/assets/user_asset_list.html:51 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:193 assets/models/cmd_filter.py:21 +#: assets/models/asset.py:170 assets/models/cmd_filter.py:21 #: assets/models/domain.py:54 assets/models/label.py:22 #: assets/templates/assets/asset_detail.html:112 -#: assets/templates/assets/user_asset_list.html:174 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:199 assets/templates/assets/asset_detail.html:68 +#: assets/models/asset.py:176 assets/templates/assets/asset_detail.html:68 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:200 assets/templates/assets/asset_detail.html:120 +#: assets/models/asset.py:177 assets/templates/assets/asset_detail.html:120 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:84 +#: assets/models/asset.py:180 assets/templates/assets/asset_detail.html:84 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:204 assets/templates/assets/asset_detail.html:88 +#: assets/models/asset.py:181 assets/templates/assets/asset_detail.html:88 msgid "Model" msgstr "型号" -#: assets/models/asset.py:205 assets/templates/assets/asset_detail.html:116 +#: assets/models/asset.py:182 assets/templates/assets/asset_detail.html:116 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:207 +#: assets/models/asset.py:184 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:208 +#: assets/models/asset.py:185 #: xpack/plugins/license/templates/license/license_detail.html:80 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:209 +#: assets/models/asset.py:186 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:210 +#: assets/models/asset.py:187 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:211 assets/templates/assets/asset_detail.html:96 +#: assets/models/asset.py:188 assets/templates/assets/asset_detail.html:96 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:212 +#: assets/models/asset.py:189 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:213 +#: assets/models/asset.py:190 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:215 assets/templates/assets/asset_detail.html:108 -#: assets/templates/assets/user_asset_list.html:171 +#: assets/models/asset.py:192 assets/templates/assets/asset_detail.html:108 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:216 +#: assets/models/asset.py:193 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:217 +#: assets/models/asset.py:194 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:218 +#: assets/models/asset.py:195 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:220 assets/templates/assets/asset_create.html:46 +#: assets/models/asset.py:197 assets/templates/assets/asset_create.html:46 #: assets/templates/assets/asset_detail.html:227 templates/_nav.html:26 msgid "Labels" msgstr "标签管理" @@ -976,7 +969,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:83 +#: assets/models/cluster.py:22 users/models/user.py:343 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -1002,7 +995,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:453 +#: users/models/user.py:451 msgid "System" msgstr "系统" @@ -1109,7 +1102,7 @@ msgstr "默认资产组" #: audits/templates/audits/password_change_log_list.html:50 #: ops/templates/ops/command_execution_list.html:35 #: ops/templates/ops/command_execution_list.html:60 -#: perms/forms/asset_permission.py:62 perms/forms/remote_app_permission.py:31 +#: perms/forms/asset_permission.py:63 perms/forms/remote_app_permission.py:31 #: perms/models/base.py:36 #: perms/templates/perms/asset_permission_create_update.html:41 #: perms/templates/perms/asset_permission_list.html:46 @@ -1121,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:38 users/models/user.py:441 users/serializers/v1.py:105 -#: users/templates/users/user_group_detail.html:78 +#: users/models/user.py:121 users/models/user.py:439 +#: users/serializers/v1.py:109 users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_list.html:36 users/views/user.py:251 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 @@ -1130,7 +1123,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:245 +#: assets/models/label.py:19 assets/models/node.py:244 #: assets/templates/assets/label_list.html:15 settings/models.py:30 msgid "Value" msgstr "值" @@ -1139,11 +1132,11 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:244 +#: assets/models/node.py:243 msgid "Key" msgstr "键" -#: assets/models/node.py:302 +#: assets/models/node.py:301 msgid "New node" msgstr "新节点" @@ -1165,13 +1158,13 @@ msgstr "手动登录" #: assets/views/asset.py:57 assets/views/asset.py:106 assets/views/asset.py:133 #: assets/views/asset.py:173 assets/views/asset.py:203 #: assets/views/cmd_filter.py:31 assets/views/cmd_filter.py:48 -#: assets/views/cmd_filter.py:65 assets/views/cmd_filter.py:82 -#: assets/views/cmd_filter.py:102 assets/views/cmd_filter.py:136 -#: assets/views/cmd_filter.py:170 assets/views/domain.py:30 -#: assets/views/domain.py:47 assets/views/domain.py:64 -#: assets/views/domain.py:78 assets/views/domain.py:104 -#: assets/views/domain.py:133 assets/views/domain.py:153 -#: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:72 +#: assets/views/cmd_filter.py:66 assets/views/cmd_filter.py:84 +#: assets/views/cmd_filter.py:104 assets/views/cmd_filter.py:138 +#: assets/views/cmd_filter.py:173 assets/views/domain.py:30 +#: assets/views/domain.py:47 assets/views/domain.py:65 +#: assets/views/domain.py:80 assets/views/domain.py:106 +#: assets/views/domain.py:135 assets/views/domain.py:156 +#: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:73 #: assets/views/system_user.py:29 assets/views/system_user.py:46 #: assets/views/system_user.py:63 assets/views/system_user.py:79 #: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:68 @@ -1228,7 +1221,7 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:66 users/forms.py:263 -#: users/models/user.py:94 users/templates/users/first_login.html:42 +#: users/models/user.py:354 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 #: users/templates/users/user_profile.html:68 #: users/templates/users/user_profile_update.html:43 @@ -1363,8 +1356,6 @@ msgstr "选择资产" #: assets/templates/assets/_asset_group_bulk_update_modal.html:21 #: assets/templates/assets/cmd_filter_detail.html:89 #: assets/templates/assets/cmd_filter_list.html:26 -#: assets/templates/assets/user_asset_list.html:47 -#: users/templates/users/user_granted_asset.html:47 msgid "System users" msgstr "系统用户" @@ -1574,7 +1565,7 @@ msgid "Replace node assets admin user with this" msgstr "替换资产的管理员" #: assets/templates/assets/admin_user_detail.html:91 -#: perms/templates/perms/asset_permission_asset.html:116 +#: perms/templates/perms/asset_permission_asset.html:103 #: xpack/plugins/change_auth_plan/forms.py:110 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:112 msgid "Select nodes" @@ -1705,8 +1696,7 @@ msgid "Date joined" msgstr "创建日期" #: assets/templates/assets/asset_detail.html:150 -#: assets/templates/assets/user_asset_list.html:46 -#: perms/models/asset_permission.py:104 perms/models/base.py:38 +#: 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 #: perms/templates/perms/remote_app_permission_create_update.html:54 @@ -1714,8 +1704,6 @@ msgstr "创建日期" #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:144 -#: users/templates/users/user_granted_asset.html:46 -#: users/templates/users/user_group_granted_asset.html:46 #: users/templates/users/user_profile.html:63 msgid "Active" msgstr "激活中" @@ -1881,7 +1869,7 @@ msgid "Create command filter" msgstr "创建命令过滤器" #: assets/templates/assets/cmd_filter_rule_list.html:33 -#: assets/views/cmd_filter.py:103 +#: assets/views/cmd_filter.py:105 msgid "Command filter rule list" msgstr "命令过滤器规则列表" @@ -1908,7 +1896,7 @@ msgid "Gateway list" msgstr "网关列表" #: assets/templates/assets/domain_gateway_list.html:56 -#: assets/views/domain.py:134 +#: assets/views/domain.py:136 msgid "Create gateway" msgstr "创建网关" @@ -2056,19 +2044,19 @@ msgstr "批量更新资产" msgid "Command filter list" msgstr "命令过滤器列表" -#: assets/views/cmd_filter.py:66 +#: assets/views/cmd_filter.py:67 msgid "Update command filter" msgstr "更新命令过滤器" -#: assets/views/cmd_filter.py:83 +#: assets/views/cmd_filter.py:85 msgid "Command filter detail" msgstr "命令过滤器详情" -#: assets/views/cmd_filter.py:137 +#: assets/views/cmd_filter.py:139 msgid "Create command filter rule" msgstr "创建命令过滤器规则" -#: assets/views/cmd_filter.py:171 +#: assets/views/cmd_filter.py:174 msgid "Update command filter rule" msgstr "更新命令过滤器规则" @@ -2076,19 +2064,19 @@ msgstr "更新命令过滤器规则" msgid "Domain list" msgstr "网域列表" -#: assets/views/domain.py:65 +#: assets/views/domain.py:66 msgid "Update domain" msgstr "更新网域" -#: assets/views/domain.py:79 +#: assets/views/domain.py:81 msgid "Domain detail" msgstr "网域详情" -#: assets/views/domain.py:105 +#: assets/views/domain.py:107 msgid "Domain gateway list" msgstr "域网关列表" -#: assets/views/domain.py:154 +#: assets/views/domain.py:157 msgid "Update gateway" msgstr "创建网关" @@ -2096,11 +2084,11 @@ msgstr "创建网关" msgid "Label list" msgstr "标签列表" -#: assets/views/label.py:55 +#: assets/views/label.py:56 msgid "Tips: Avoid using label names reserved internally: {}" msgstr "提示: 请避免使用内部预留标签名: {}" -#: assets/views/label.py:73 +#: assets/views/label.py:74 msgid "Update label" msgstr "更新标签" @@ -2218,7 +2206,7 @@ msgstr "Agent" #: audits/models.py:99 audits/templates/audits/login_log_list.html:56 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:175 users/models/user.py:86 +#: users/forms.py:175 users/models/user.py:346 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" @@ -2431,7 +2419,7 @@ msgstr "代码错误" #: authentication/templates/authentication/login.html:27 #: authentication/templates/authentication/login_otp.html:27 #: users/templates/users/reset_password.html:25 -#: xpack/plugins/interface/models.py:39 +#: xpack/plugins/interface/models.py:36 msgid "Welcome to the Jumpserver open source fortress" msgstr "欢迎使用Jumpserver开源堡垒机" @@ -2996,71 +2984,71 @@ msgstr "命令执行" msgid "Organization" msgstr "组织" -#: perms/api/user_permission.py:206 +#: perms/api/mixin.py:128 msgid "ungrouped" msgstr "未分组" -#: perms/api/user_permission.py:211 +#: perms/api/mixin.py:133 msgid "empty" msgstr "空" -#: perms/forms/asset_permission.py:65 perms/forms/remote_app_permission.py:34 -#: perms/models/asset_permission.py:102 perms/models/base.py:37 +#: perms/forms/asset_permission.py:66 perms/forms/remote_app_permission.py:34 +#: perms/models/asset_permission.py:113 perms/models/base.py:37 #: perms/templates/perms/asset_permission_list.html:47 #: perms/templates/perms/asset_permission_list.html:67 #: perms/templates/perms/asset_permission_list.html:114 #: perms/templates/perms/remote_app_permission_list.html:16 #: templates/_nav.html:14 users/forms.py:286 users/models/group.py:26 -#: users/models/user.py:70 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:330 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:213 #: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User group" msgstr "用户组" -#: perms/forms/asset_permission.py:81 +#: perms/forms/asset_permission.py:82 msgid "" "Tips: The RDP protocol does not support separate controls for uploading or " "downloading files" msgstr "提示:RDP 协议不支持单独控制上传或下载文件" -#: perms/forms/asset_permission.py:91 perms/forms/remote_app_permission.py:47 +#: perms/forms/asset_permission.py:92 perms/forms/remote_app_permission.py:47 msgid "User or group at least one required" msgstr "用户和用户组至少选一个" -#: perms/forms/asset_permission.py:100 +#: perms/forms/asset_permission.py:101 msgid "Asset or group at least one required" msgstr "资产和节点至少选一个" -#: perms/models/asset_permission.py:27 settings/forms.py:143 +#: perms/models/asset_permission.py:29 settings/forms.py:143 msgid "All" msgstr "全部" -#: perms/models/asset_permission.py:29 +#: perms/models/asset_permission.py:31 msgid "Upload file" msgstr "上传文件" -#: perms/models/asset_permission.py:30 +#: perms/models/asset_permission.py:32 msgid "Download file" msgstr "下载文件" -#: perms/models/asset_permission.py:31 +#: perms/models/asset_permission.py:33 msgid "Upload download" msgstr "上传下载" -#: perms/models/asset_permission.py:80 +#: perms/models/asset_permission.py:82 msgid "Actions" msgstr "动作" -#: perms/models/asset_permission.py:84 perms/models/asset_permission.py:114 +#: perms/models/asset_permission.py:86 perms/models/asset_permission.py:125 #: templates/_nav.html:44 msgid "Asset permission" msgstr "资产授权" -#: perms/models/asset_permission.py:105 perms/models/base.py:40 +#: perms/models/asset_permission.py:116 perms/models/base.py:40 #: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:102 users/templates/users/user_detail.html:107 +#: users/models/user.py:362 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:116 msgid "Date expired" msgstr "失效日期" @@ -3087,11 +3075,11 @@ msgstr "用户或用户组" msgid "Assets and node" msgstr "资产或节点" -#: perms/templates/perms/asset_permission_asset.html:80 +#: perms/templates/perms/asset_permission_asset.html:70 msgid "Add asset to this permission" msgstr "添加资产" -#: perms/templates/perms/asset_permission_asset.html:97 +#: perms/templates/perms/asset_permission_asset.html:84 #: perms/templates/perms/asset_permission_detail.html:157 #: perms/templates/perms/asset_permission_user.html:97 #: perms/templates/perms/asset_permission_user.html:125 @@ -3107,11 +3095,11 @@ msgstr "添加资产" msgid "Add" msgstr "添加" -#: perms/templates/perms/asset_permission_asset.html:108 +#: perms/templates/perms/asset_permission_asset.html:95 msgid "Add node to this permission" msgstr "添加节点" -#: perms/templates/perms/asset_permission_asset.html:125 +#: perms/templates/perms/asset_permission_asset.html:112 #: users/templates/users/user_detail.html:230 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:121 msgid "Join" @@ -3208,13 +3196,13 @@ msgstr "添加用户组" #: perms/views/asset_permission.py:33 perms/views/asset_permission.py:64 #: perms/views/asset_permission.py:81 perms/views/asset_permission.py:98 -#: perms/views/asset_permission.py:135 perms/views/asset_permission.py:168 +#: perms/views/asset_permission.py:135 perms/views/asset_permission.py:169 #: perms/views/remote_app_permission.py:33 #: perms/views/remote_app_permission.py:49 -#: perms/views/remote_app_permission.py:65 -#: perms/views/remote_app_permission.py:79 -#: perms/views/remote_app_permission.py:106 -#: perms/views/remote_app_permission.py:143 templates/_nav.html:41 +#: perms/views/remote_app_permission.py:66 +#: perms/views/remote_app_permission.py:81 +#: perms/views/remote_app_permission.py:108 +#: perms/views/remote_app_permission.py:145 templates/_nav.html:41 #: xpack/plugins/orgs/templates/orgs/org_list.html:21 msgid "Perms" msgstr "权限管理" @@ -3239,7 +3227,7 @@ msgstr "资产授权详情" msgid "Asset permission user list" msgstr "资产授权用户列表" -#: perms/views/asset_permission.py:169 +#: perms/views/asset_permission.py:170 msgid "Asset permission asset list" msgstr "资产授权资产列表" @@ -3251,19 +3239,19 @@ msgstr "远程应用授权列表" msgid "Create RemoteApp permission" msgstr "创建远程应用授权规则" -#: perms/views/remote_app_permission.py:66 +#: perms/views/remote_app_permission.py:67 msgid "Update RemoteApp permission" msgstr "更新远程应用授权规则" -#: perms/views/remote_app_permission.py:80 +#: perms/views/remote_app_permission.py:82 msgid "RemoteApp permission detail" msgstr "远程应用授权详情" -#: perms/views/remote_app_permission.py:107 +#: perms/views/remote_app_permission.py:109 msgid "RemoteApp permission user list" msgstr "远程应用授权用户列表" -#: perms/views/remote_app_permission.py:144 +#: perms/views/remote_app_permission.py:146 msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" @@ -3466,35 +3454,45 @@ msgstr "批量命令" msgid "Allow user batch execute commands" msgstr "允许用户批量执行命令" -#: settings/forms.py:198 +#: settings/forms.py:196 +msgid "Service account registration" +msgstr "终端注册" + +#: settings/forms.py:197 +msgid "" +"Allow using bootstrap token register service account, when terminal setup, " +"can disable it" +msgstr "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止" + +#: settings/forms.py:203 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/forms.py:202 +#: settings/forms.py:207 msgid "No logon interval" msgstr "禁止登录时间间隔" -#: settings/forms.py:204 +#: settings/forms.py:209 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/forms.py:211 +#: settings/forms.py:216 msgid "Connection max idle time" msgstr "SSH最大空闲时间" -#: settings/forms.py:213 +#: settings/forms.py:218 msgid "" "If idle time more than it, disconnect connection(only ssh now) Unit: minute" msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)" -#: settings/forms.py:219 +#: settings/forms.py:224 msgid "Password expiration time" msgstr "密码过期时间" -#: settings/forms.py:221 +#: settings/forms.py:226 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -3504,81 +3502,81 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/forms.py:230 +#: settings/forms.py:235 msgid "Password minimum length" msgstr "密码最小长度 " -#: settings/forms.py:234 +#: settings/forms.py:239 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: settings/forms.py:236 +#: settings/forms.py:241 msgid "" "After opening, the user password changes and resets must contain uppercase " "letters" msgstr "开启后,用户密码修改、重置必须包含大写字母" -#: settings/forms.py:241 +#: settings/forms.py:246 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: settings/forms.py:242 +#: settings/forms.py:247 msgid "" "After opening, the user password changes and resets must contain lowercase " "letters" msgstr "开启后,用户密码修改、重置必须包含小写字母" -#: settings/forms.py:247 +#: settings/forms.py:252 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: settings/forms.py:248 +#: settings/forms.py:253 msgid "" "After opening, the user password changes and resets must contain numeric " "characters" msgstr "开启后,用户密码修改、重置必须包含数字字符" -#: settings/forms.py:253 +#: settings/forms.py:258 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: settings/forms.py:254 +#: settings/forms.py:259 msgid "" "After opening, the user password changes and resets must contain special " "characters" msgstr "开启后,用户密码修改、重置必须包含特殊字符" -#: settings/forms.py:261 +#: settings/forms.py:266 msgid "Create user email subject" msgstr "创建用户邮件的主题" -#: settings/forms.py:262 +#: settings/forms.py:267 msgid "" "Tips: When creating a user, send the subject of the email (eg:Create account " "successfully)" msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" -#: settings/forms.py:266 +#: settings/forms.py:271 msgid "Create user honorific" msgstr "创建用户邮件的敬语" -#: settings/forms.py:267 +#: settings/forms.py:272 msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" -#: settings/forms.py:272 +#: settings/forms.py:277 msgid "Create user email content" msgstr "创建用户邮件的内容" -#: settings/forms.py:273 +#: settings/forms.py:278 msgid "Tips:When creating a user, send the content of the email" msgstr "提示: 创建用户时,发送设置密码邮件的内容" -#: settings/forms.py:276 +#: settings/forms.py:281 msgid "Signature" msgstr "署名" -#: settings/forms.py:277 +#: settings/forms.py:282 msgid "Tips: Email signature (eg:jumpserver)" msgstr "提示: 邮件的署名 (例如: jumpserver)" @@ -3596,7 +3594,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:39 -#: users/models/user.py:66 users/templates/users/user_detail.html:71 +#: users/models/user.py:326 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -3930,7 +3928,7 @@ msgstr "" " " #: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:45 -#: users/views/group.py:62 users/views/group.py:79 users/views/group.py:96 +#: users/views/group.py:63 users/views/group.py:81 users/views/group.py:98 #: users/views/login.py:154 users/views/user.py:68 users/views/user.py:85 #: users/views/user.py:129 users/views/user.py:196 users/views/user.py:218 #: users/views/user.py:270 users/views/user.py:311 @@ -4384,7 +4382,7 @@ msgstr "你没有权限" msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:33 users/models/user.py:74 +#: users/forms.py:33 users/models/user.py:334 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4412,7 +4410,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:91 +#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:95 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" @@ -4497,57 +4495,57 @@ msgstr "复制你的公钥到这里" msgid "Select users" msgstr "选择用户" -#: users/models/user.py:37 users/models/user.py:449 +#: users/models/user.py:50 users/templates/users/user_update.html:22 +#: users/views/login.py:46 users/views/login.py:107 users/views/user.py:283 +msgid "User auth from {}, go there change password" +msgstr "用户认证源来自 {}, 请去相应系统修改密码" + +#: users/models/user.py:120 users/models/user.py:447 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:39 +#: users/models/user.py:122 msgid "Application" msgstr "应用程序" -#: users/models/user.py:40 +#: users/models/user.py:123 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:43 users/templates/users/user_profile.html:92 +#: users/models/user.py:281 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:159 #: users/templates/users/user_profile.html:162 msgid "Disable" msgstr "禁用" -#: users/models/user.py:44 users/templates/users/user_profile.html:90 +#: users/models/user.py:282 users/templates/users/user_profile.html:90 #: users/templates/users/user_profile.html:166 msgid "Enable" msgstr "启用" -#: users/models/user.py:45 users/templates/users/user_profile.html:88 +#: users/models/user.py:283 users/templates/users/user_profile.html:88 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:77 +#: users/models/user.py:337 msgid "Avatar" msgstr "头像" -#: users/models/user.py:80 users/templates/users/user_detail.html:82 +#: users/models/user.py:340 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:109 users/templates/users/user_detail.html:103 +#: users/models/user.py:369 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" -#: users/models/user.py:113 +#: users/models/user.py:373 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:139 users/templates/users/user_update.html:22 -#: users/views/login.py:46 users/views/login.py:107 users/views/user.py:283 -msgid "User auth from {}, go there change password" -msgstr "用户认证源来自 {}, 请去相应系统修改密码" - -#: users/models/user.py:452 +#: users/models/user.py:450 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -4583,7 +4581,7 @@ msgstr "头像路径" msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/v1.py:63 +#: users/serializers/v1.py:67 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -4628,7 +4626,7 @@ msgid "Import user groups" msgstr "导入用户组" #: users/templates/users/_user_groups_update_modal.html:4 -#: users/views/group.py:63 +#: users/views/group.py:64 msgid "Update user group" msgstr "更新用户组" @@ -4715,14 +4713,14 @@ msgid "Reset password" msgstr "重置密码" #: users/templates/users/reset_password.html:59 -#: users/templates/users/user_create.html:15 +#: users/templates/users/user_create.html:13 #: users/templates/users/user_password_update.html:61 #: users/templates/users/user_update.html:13 msgid "Your password must satisfy" msgstr "您的密码必须满足:" #: users/templates/users/reset_password.html:60 -#: users/templates/users/user_create.html:16 +#: users/templates/users/user_create.html:14 #: users/templates/users/user_password_update.html:62 #: users/templates/users/user_update.html:14 msgid "Password strength" @@ -4733,42 +4731,42 @@ msgid "Password again" msgstr "再次输入密码" #: users/templates/users/reset_password.html:105 -#: users/templates/users/user_create.html:35 +#: users/templates/users/user_create.html:33 #: users/templates/users/user_password_update.html:99 #: users/templates/users/user_update.html:46 msgid "Very weak" msgstr "很弱" #: users/templates/users/reset_password.html:106 -#: users/templates/users/user_create.html:36 +#: users/templates/users/user_create.html:34 #: users/templates/users/user_password_update.html:100 #: users/templates/users/user_update.html:47 msgid "Weak" msgstr "弱" #: users/templates/users/reset_password.html:107 -#: users/templates/users/user_create.html:37 +#: users/templates/users/user_create.html:35 #: users/templates/users/user_password_update.html:101 #: users/templates/users/user_update.html:48 msgid "Normal" msgstr "正常" #: users/templates/users/reset_password.html:108 -#: users/templates/users/user_create.html:38 +#: users/templates/users/user_create.html:36 #: users/templates/users/user_password_update.html:102 #: users/templates/users/user_update.html:49 msgid "Medium" msgstr "一般" #: users/templates/users/reset_password.html:109 -#: users/templates/users/user_create.html:39 +#: users/templates/users/user_create.html:37 #: users/templates/users/user_password_update.html:103 #: users/templates/users/user_update.html:50 msgid "Strong" msgstr "强" #: users/templates/users/reset_password.html:110 -#: users/templates/users/user_create.html:40 +#: users/templates/users/user_create.html:38 #: users/templates/users/user_password_update.html:104 #: users/templates/users/user_update.html:51 msgid "Very strong" @@ -4880,7 +4878,7 @@ msgstr "重置用户MFA成功" #: users/templates/users/user_group_detail.html:22 #: users/templates/users/user_group_granted_asset.html:18 -#: users/views/group.py:80 +#: users/views/group.py:82 msgid "User group detail" msgstr "用户组详情" @@ -5222,7 +5220,7 @@ msgstr "密码或密钥不合法" msgid "User group list" msgstr "用户组列表" -#: users/views/group.py:97 +#: users/views/group.py:99 msgid "User group granted asset" msgstr "用户组授权资产" diff --git a/apps/settings/forms.py b/apps/settings/forms.py index 78fab4801..c6f51e646 100644 --- a/apps/settings/forms.py +++ b/apps/settings/forms.py @@ -192,6 +192,11 @@ class SecuritySettingForm(BaseForm): required=False, label=_("Batch execute commands"), help_text=_("Allow user batch execute commands") ) + SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField( + required=False, label=_("Service account registration"), + help_text=_("Allow using bootstrap token register service account, " + "when terminal setup, can disable it") + ) # limit login count SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( min_value=3, max_value=99999, diff --git a/apps/terminal/serializers_v2/terminal.py b/apps/terminal/serializers_v2/terminal.py index 2ecb4e1ee..c7ebe682c 100644 --- a/apps/terminal/serializers_v2/terminal.py +++ b/apps/terminal/serializers_v2/terminal.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +from django.conf import settings from rest_framework import serializers from common.utils import get_request_ip @@ -27,6 +28,9 @@ class TerminalSerializer(serializers.ModelSerializer): valid = super().is_valid(raise_exception=raise_exception) if not valid: return valid + if not settings.SECURITY_SERVICE_ACCOUNT_REGISTRATION: + error = {"error": "service account registration disabled"} + raise serializers.ValidationError(error) data = {'name': self.validated_data.get('name')} kwargs = {'data': data} if self.instance and self.instance.user: