diff --git a/apps/applications/templates/applications/remote_app_create_update.html b/apps/applications/templates/applications/remote_app_create_update.html index ecd7254a1..897126f98 100644 --- a/apps/applications/templates/applications/remote_app_create_update.html +++ b/apps/applications/templates/applications/remote_app_create_update.html @@ -107,6 +107,21 @@ function hiddenFields(){ }); $('.' + app_type + '-fields').removeClass('hidden'); } +function constructParams(data) { + var typeList = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']; + var params = {}; + 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; +} $(document).ready(function () { $('.select2').select2({ closeOnSelect: true @@ -118,6 +133,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) diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py index eb8594e4a..d3537b20c 100644 --- a/apps/assets/api/label.py +++ b/apps/assets/api/label.py @@ -13,11 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rest_framework_bulk import BulkModelViewSet from rest_framework.pagination import LimitOffsetPagination from django.db.models import Count from common.utils import get_logger +from orgs.mixins import OrgBulkModelViewSet from ..hands import IsOrgAdmin from ..models import Label from .. import serializers @@ -27,7 +27,7 @@ logger = get_logger(__file__) __all__ = ['LabelViewSet'] -class LabelViewSet(BulkModelViewSet): +class LabelViewSet(OrgBulkModelViewSet): filter_fields = ("name", "value") search_fields = filter_fields permission_classes = (IsOrgAdmin,) 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/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/assets/migrations/0035_auto_20190711_2018.py b/apps/assets/migrations/0035_auto_20190711_2018.py new file mode 100644 index 000000000..9dcbad1db --- /dev/null +++ b/apps/assets/migrations/0035_auto_20190711_2018.py @@ -0,0 +1,34 @@ +# Generated by Django 2.1.7 on 2019-07-11 12:18 + +import common.fields.model +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0034_auto_20190705_1348'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='authbook', + name='private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='gateway', + name='private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='systemuser', + name='private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), + ), + ] 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/base.py b/apps/assets/models/base.py index b372bc9fd..75f81917f 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -28,7 +28,7 @@ class AssetUser(OrgModelMixin): name = models.CharField(max_length=128, verbose_name=_('Name')) username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric]) password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) + private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) 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..0efc09801 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -21,19 +21,15 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): model = AdminUser fields = [ 'id', 'name', 'username', 'password', 'private_key', 'public_key', - 'comment', 'connectivity_amount', 'assets_amount', - 'date_created', 'date_updated', 'created_by', + 'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by', ] + read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount'] extra_kwargs = { 'password': {"write_only": True}, 'private_key': {"write_only": True}, 'public_key': {"write_only": True}, - 'date_created': {'read_only': True}, - 'date_updated': {'read_only': True}, - 'created_by': {'read_only': True}, 'assets_amount': {'label': _('Asset')}, - 'connectivity_amount': {'label': _('Connectivity')}, } 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/serializers/domain.py b/apps/assets/serializers/domain.py index cda208b9f..68feccb41 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): @@ -14,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 @@ -26,14 +31,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/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/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/_asset_user_auth_view_modal.html b/apps/assets/templates/assets/_asset_user_auth_view_modal.html index 6fbd48fcd..417e1021d 100644 --- a/apps/assets/templates/assets/_asset_user_auth_view_modal.html +++ b/apps/assets/templates/assets/_asset_user_auth_view_modal.html @@ -70,7 +70,7 @@ function showAuth() { var msg = "{% trans 'Get auth info error' %}"; toastr.error(msg) }; - APIUpdateAttr({ + requestApi({ url: url, method: "GET", success: success, diff --git a/apps/assets/templates/assets/_asset_user_list.html b/apps/assets/templates/assets/_asset_user_list.html index 381aec13d..f76754391 100644 --- a/apps/assets/templates/assets/_asset_user_list.html +++ b/apps/assets/templates/assets/_asset_user_list.html @@ -141,7 +141,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, method: 'GET', success: success, diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index 93aed7d89..9a4004060 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -235,7 +235,7 @@ function onRename(event, treeId, treeNode, isCancel){ if (isCancel){ return } - APIUpdateAttr({ + requestApi({ url: url, body: JSON.stringify(data), method: "PATCH", @@ -274,7 +274,7 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) { var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id); var body = {nodes: treeNodesIds}; - APIUpdateAttr({ + requestApi({ url: the_url, method: "PUT", body: JSON.stringify(body) @@ -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/_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'); diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index 7c97259ab..9ca433930 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -88,7 +88,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, method: 'GET', success: success, diff --git a/apps/assets/templates/assets/admin_user_detail.html b/apps/assets/templates/assets/admin_user_detail.html index f00e2352a..9e3365509 100644 --- a/apps/assets/templates/assets/admin_user_detail.html +++ b/apps/assets/templates/assets/admin_user_detail.html @@ -131,7 +131,7 @@ function replaceNodeAssetsAdminUser(nodes) { // clear jumpserver.groups_selected jumpserver.nodes_selected = {}; }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success 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_asset_user_list.html b/apps/assets/templates/assets/asset_asset_user_list.html index 39e4816b9..bf3cba583 100644 --- a/apps/assets/templates/assets/asset_asset_user_list.html +++ b/apps/assets/templates/assets/asset_asset_user_list.html @@ -84,7 +84,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, method: 'GET', success: success, 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/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index ad635ca5b..760cc7e6d 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -70,7 +70,7 @@ {% trans 'Protocol' %} - {{ asset.protocols }} + {{ asset.protocols }} {% trans 'Admin user' %}: @@ -267,7 +267,7 @@ function updateAssetNodes(nodes) { // clear jumpserver.groups_selected jumpserver.nodes_selected = {}; }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -282,7 +282,7 @@ function refreshAssetHardware() { var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); window.open(url, '', 'width=800,height=600') }; - APIUpdateAttr({ + requestApi({ url: the_url, success: success, method: 'GET' @@ -306,7 +306,7 @@ $(document).ready(function () { }; var success = '{% trans "Update successfully!" %}'; var status = $(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").text(); - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success_message: success @@ -360,7 +360,7 @@ $(document).ready(function () { window.open(url, '', 'width=800,height=600') }; - APIUpdateAttr({ + requestApi({ url: the_url, method: 'GET', success: success diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 3b9df3373..0cdcb176a 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(); @@ -361,7 +360,7 @@ $(document).ready(function(){ setTimeout( function () { window.location.reload();}, 500); } - APIUpdateAttr({ + requestApi({ url: the_url, method: 'PATCH', body: JSON.stringify(data), @@ -378,7 +377,7 @@ $(document).ready(function(){ setTimeout( function () { window.location.reload();}, 300); } - APIUpdateAttr({ + requestApi({ url: the_url, method: 'PATCH', body: JSON.stringify(data), @@ -398,7 +397,7 @@ $(document).ready(function(){ },function () { function success(data) { url = setUrlParam(the_url, 'spm', data.spm); - APIUpdateAttr({ + requestApi({ url:url, method:'DELETE', success:refreshTag, @@ -411,7 +410,7 @@ $(document).ready(function(){ var msg = "{% trans 'Asset Deleting failed.' %}"; swal("{% trans 'Asset Delete' %}", msg, "error"); } - APIUpdateAttr({ + requestApi({ url: "{% url 'api-common:resources-cache' %}", method:'POST', body:JSON.stringify(data), @@ -429,7 +428,7 @@ $(document).ready(function(){ var url = "{% url 'assets:asset-bulk-update' %}"; location.href= setUrlParam(url, 'spm', data.spm); } - APIUpdateAttr({ + requestApi({ url: "{% url 'api-common:resources-cache' %}", method:'POST', body:JSON.stringify(data), @@ -453,7 +452,7 @@ $(document).ready(function(){ asset_table.ajax.reload() }; - APIUpdateAttr({ + requestApi({ 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/', 'method': 'PUT', 'body': JSON.stringify(data), @@ -501,7 +500,7 @@ $(document).ready(function(){ url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); } - APIUpdateAttr({ + requestApi({ 'url': url, 'method': 'PUT', 'body': JSON.stringify(data), @@ -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') + } + requestApi({ + 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') + } + requestApi({ + url: the_url, + method: "GET", + success: success, + flash_message: false + }); + }) 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/templates/assets/cmd_filter_detail.html b/apps/assets/templates/assets/cmd_filter_detail.html index ee68ff2f6..b98828f4e 100644 --- a/apps/assets/templates/assets/cmd_filter_detail.html +++ b/apps/assets/templates/assets/cmd_filter_detail.html @@ -136,7 +136,7 @@ function updateCMDFilterSystemUsers(system_users) { var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), method: 'PATCH', 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/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/templates/assets/domain_gateway_list.html b/apps/assets/templates/assets/domain_gateway_list.html index d621fb0ec..eb348141e 100644 --- a/apps/assets/templates/assets/domain_gateway_list.html +++ b/apps/assets/templates/assets/domain_gateway_list.html @@ -134,7 +134,7 @@ $(document).ready(function(){ var data = $("#test_gateway_form").serializeObject(); var uid = data.gateway_id; var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid); - APIUpdateAttr({ + requestApi({ url: the_url, method: "POST", body: JSON.stringify({'port': parseInt(data.port)}), 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/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/templates/assets/system_user_assets.html b/apps/assets/templates/assets/system_user_assets.html index 546111130..5818e4ce6 100644 --- a/apps/assets/templates/assets/system_user_assets.html +++ b/apps/assets/templates/assets/system_user_assets.html @@ -146,7 +146,7 @@ function updateSystemUserNode(nodes) { // clear jumpserver.nodes_selected jumpserver.nodes_selected = {}; }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -206,7 +206,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', @@ -226,7 +226,7 @@ $(document).ready(function () { var error = function (data) { alert(data) }; - APIUpdateAttr({ + requestApi({ url: the_url, method: 'GET', success: success, @@ -243,7 +243,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/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 55f625d81..d9ff1a641 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -212,7 +212,7 @@ function updateCommandFilters(command_filters) { var success = function(data) { location.reload(); }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success @@ -235,7 +235,7 @@ $(document).ready(function () { var body = { 'auto_push': checked }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body) }); @@ -254,7 +254,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, method: 'GET', success: success, @@ -268,7 +268,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') }; - APIUpdateAttr({ + requestApi({ url: the_url, method: 'GET', success: success, diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 680bfa421..621e18201 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() }; @@ -182,7 +182,7 @@ $(document).ready(function(){ swal("{% trans 'System Users Delete' %}", msg, "error"); }; var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list); - APIUpdateAttr({url: url_delete, method: 'DELETE', success: success, error: fail}); + requestApi({url: url_delete, method: 'DELETE', success: success, error: fail}); $data_table.ajax.reload(); jumpserver.checked = false; }); diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index 23d0b34ed..d021df0d7 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -11,48 +11,7 @@ {% block content %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- - -
- - - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Active' %}{% trans 'System users' %}{% trans 'Action' %}
-
-
+ {% include 'users/_granted_assets.html' %}
@@ -62,130 +21,52 @@ {% block custom_foot_js %} - {% endblock %} \ No newline at end of file diff --git a/apps/assets/utils.py b/apps/assets/utils.py index b64fea35e..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 @@ -104,7 +115,7 @@ class NodeUtil: _node._assets_amount = len(_node._assets) delattr(_node, '_assets') self.stack.top._children.append(_node) - self.stack.top._all_children.extend([_node] + _node._children) + self.stack.top._all_children.extend([_node] + _node._all_children) def init(self): all_nodes = self.get_all_nodes() @@ -145,29 +156,69 @@ class NodeUtil: def nodes(self): return list(self._nodes.values()) + def get_family_by_key(self, key): + tree_nodes = set() + node = self.get_node_by_key(key) + if not node: + return [] + tree_nodes.update(node._parents) + tree_nodes.add(node) + tree_nodes.update(node._all_children) + return list(tree_nodes) + # 使用给定节点生成一颗树 # 找到他们的祖先节点 # 可选找到他们的子孙节点 - def get_family(self, nodes, with_children=False): - tree_nodes = set() - for n in nodes: - node = self.get_node_by_key(n.key) - if not node: - continue - tree_nodes.update(node._parents) - tree_nodes.add(node) - if with_children: - tree_nodes.update(node._children) - return list(tree_nodes) + def get_family(self, node): + return self.get_family_by_key(node.key) - def get_nodes_parents(self, nodes, with_self=True): + def get_family_keys_by_key(self, key): + nodes = self.get_family_by_key(key) + return [n.key for n in nodes] + + def get_some_nodes_family_by_keys(self, keys): + family = set() + for key in keys: + family.update(self.get_family_by_key(key)) + return family + + def get_some_nodes_family_keys_by_keys(self, keys): + family = self.get_some_nodes_family_by_keys(keys) + return [n.key for n in family] + + def get_nodes_parents_by_key(self, key, with_self=True): parents = set() - for n in nodes: - node = self.get_node_by_key(n.key) - parents.update(set(node._parents)) - if with_self: - parents.add(node) - return parents + node = self.get_node_by_key(key) + if not node: + return [] + parents.update(set(node._parents)) + if with_self: + parents.add(node) + return list(parents) + + def get_node_parents(self, node, with_self=True): + return self.get_nodes_parents_by_key(node.key, with_self=with_self) + + def get_nodes_parents_keys_by_key(self, key, with_self=True): + nodes = self.get_nodes_parents_by_key(key, with_self=with_self) + return [n.key for n in nodes] + + def get_all_children_by_key(self, key, with_self=True): + children = set() + node = self.get_node_by_key(key) + if not node: + return [] + children.update(set(node._all_children)) + if with_self: + children.add(node) + return list(children) + + def get_children(self, node, with_self=True): + return self.get_all_children_by_key(node.key, with_self=with_self) + + def get_children_keys_by_key(self, key, with_self=True): + nodes = self.get_all_children_by_key(key, with_self=with_self) + return [n.key for n in nodes] def test_node_tree(): 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/assets/views/cmd_filter.py b/apps/assets/views/cmd_filter.py index 354c1d852..530f4193b 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) @@ -136,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) @@ -170,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 diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index 797bae1f4..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) @@ -132,6 +134,7 @@ class DomainGatewayCreateView(PermissionsMixin, CreateView): context = { 'app': _('Assets'), 'action': _('Create gateway'), + 'type': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -152,6 +155,7 @@ class DomainGatewayUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Assets'), 'action': _('Update gateway'), + "type": "update" } kwargs.update(context) return super().get_context_data(**kwargs) 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) 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/api.py b/apps/common/api.py index bf41312d7..a3b1938be 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -4,11 +4,13 @@ import os import uuid from django.core.cache import cache +from django.views.decorators.csrf import csrf_exempt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import generics, serializers +from .http import HttpResponseTemporaryRedirect from .const import KEY_CACHE_RESOURCES_ID __all__ = [ @@ -86,3 +88,11 @@ class ResourcesIDCacheApi(APIView): cache_key = KEY_CACHE_RESOURCES_ID.format(spm) cache.set(cache_key, resources_id, 300) return Response({'spm': spm}) + + +@csrf_exempt +def redirect_plural_name_api(request, *args, **kwargs): + resource = kwargs.get("resource", "") + full_path = request.get_full_path() + full_path = full_path.replace(resource, resource+"s", 1) + return HttpResponseTemporaryRedirect(full_path) diff --git a/apps/common/http.py b/apps/common/http.py new file mode 100644 index 000000000..df6b9a78f --- /dev/null +++ b/apps/common/http.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +from django.http import HttpResponse +from django.utils.encoding import iri_to_uri + + +class HttpResponseTemporaryRedirect(HttpResponse): + status_code = 307 + + def __init__(self, redirect_to): + HttpResponse.__init__(self) + self['Location'] = iri_to_uri(redirect_to) 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/common/tree.py b/apps/common/tree.py index 13756a35d..e73b43aa6 100644 --- a/apps/common/tree.py +++ b/apps/common/tree.py @@ -46,12 +46,17 @@ class TreeNode: def __gt__(self, other): if self.isParent and not other.isParent: - return False + result = False elif not self.isParent and other.isParent: - return True - if self.pId != other.pId: - return self.pId > other.pId - return self.name > other.name + result = True + elif self.pId != other.pId: + result = self.pId > other.pId + else: + result = self.name > other.name + return result + + def __le__(self, other): + return not self.__gt__(other) def __eq__(self, other): return self.id == other.id @@ -74,7 +79,7 @@ class Tree: raise ValueError("Parent must not be node parent") node.pId = parent.id parent.isParent = True - self.nodes[node.id] = node + self.nodes[node.key] = node def get_nodes(self): return sorted(self.nodes.values()) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index fdd4827d3..c0bd771e4 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -7,6 +7,7 @@ import logging import datetime import uuid from functools import wraps +import time import copy import ipaddress @@ -179,3 +180,18 @@ def random_string(length): charset = string.ascii_letters + string.digits s = [random.choice(charset) for i in range(length)] return ''.join(s) + + +logger = get_logger(__name__) + + +def timeit(func): + def wrapper(*args, **kwargs): + logger.debug("Start call: {}".format(func.__name__)) + now = time.time() + result = func(*args, **kwargs) + using = (time.time() - now) * 1000 + msg = "Call {} end, using: {:.1f}ms".format(func.__name__, using) + logger.debug(msg) + return result + return wrapper 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/jumpserver/views.py b/apps/jumpserver/views.py index 7f7662add..9ad8f7b39 100644 --- a/apps/jumpserver/views.py +++ b/apps/jumpserver/views.py @@ -2,7 +2,7 @@ import datetime import re import time -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.conf import settings from django.views.generic import TemplateView, View from django.utils import timezone @@ -13,13 +13,14 @@ from rest_framework.response import Response from rest_framework.views import APIView from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse -from django.utils.encoding import iri_to_uri + from users.models import User from assets.models import Asset from terminal.models import Session from orgs.utils import current_org from common.permissions import PermissionsMixin, IsValidUser +from common.http import HttpResponseTemporaryRedirect class IndexView(PermissionsMixin, TemplateView): @@ -203,14 +204,6 @@ class I18NView(View): api_url_pattern = re.compile(r'^/api/(?P\w+)/(?P\w+)/(?P.*)$') -class HttpResponseTemporaryRedirect(HttpResponse): - status_code = 307 - - def __init__(self, redirect_to): - HttpResponse.__init__(self) - self['Location'] = iri_to_uri(redirect_to) - - @csrf_exempt def redirect_format_api(request, *args, **kwargs): _path, query = request.path, request.GET.urlencode() diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 3ca6363f6..9dc31d0c8 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 8406a09cd..1da5e158c 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-07-08 15:32+0800\n" +"POT-Creation-Date: 2019-07-15 14:43+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -76,17 +76,17 @@ 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:286 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:49 +#: assets/templates/assets/admin_user_list.html:47 #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 #: assets/templates/assets/label_list.html:16 #: 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 @@ -95,7 +95,7 @@ msgstr "运行参数" #: terminal/templates/terminal/command_list.html:66 #: terminal/templates/terminal/session_list.html:28 #: terminal/templates/terminal/session_list.html:72 -#: xpack/plugins/change_auth_plan/forms.py:114 +#: xpack/plugins/change_auth_plan/forms.py:115 #: xpack/plugins/change_auth_plan/models.py:413 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 @@ -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 "系统用户" @@ -139,7 +140,7 @@ msgstr "系统用户" #: assets/models/cmd_filter.py:20 assets/models/domain.py:20 #: assets/models/group.py:20 assets/models/label.py:18 #: assets/templates/assets/admin_user_detail.html:56 -#: assets/templates/assets/admin_user_list.html:47 +#: assets/templates/assets/admin_user_list.html:45 #: assets/templates/assets/cmd_filter_detail.html:61 #: assets/templates/assets/cmd_filter_list.html:24 #: assets/templates/assets/domain_detail.html:56 @@ -166,14 +167,14 @@ 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 #: users/templates/users/user_list.html:35 #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:53 -#: xpack/plugins/change_auth_plan/forms.py:97 +#: xpack/plugins/change_auth_plan/forms.py:98 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 @@ -205,7 +206,7 @@ msgstr "参数" #: applications/models/remote_app.py:43 #: applications/templates/applications/remote_app_detail.html:77 -#: assets/models/asset.py:151 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:152 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,12 +258,12 @@ 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:153 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 #: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72 -#: assets/templates/assets/admin_user_list.html:53 +#: assets/templates/assets/admin_user_list.html:51 #: assets/templates/assets/asset_detail.html:132 #: assets/templates/assets/cmd_filter_detail.html:65 #: assets/templates/assets/cmd_filter_list.html:27 @@ -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 @@ -412,8 +412,8 @@ msgstr "详情" #: applications/templates/applications/remote_app_list.html:56 #: assets/templates/assets/_asset_user_list.html:70 #: assets/templates/assets/admin_user_detail.html:24 -#: assets/templates/assets/admin_user_list.html:29 -#: assets/templates/assets/admin_user_list.html:114 +#: assets/templates/assets/admin_user_list.html:27 +#: assets/templates/assets/admin_user_list.html:112 #: assets/templates/assets/asset_detail.html:27 #: assets/templates/assets/asset_list.html:78 #: assets/templates/assets/asset_list.html:169 @@ -456,7 +456,7 @@ msgstr "更新" #: applications/templates/applications/remote_app_detail.html:25 #: applications/templates/applications/remote_app_list.html:57 #: assets/templates/assets/admin_user_detail.html:28 -#: assets/templates/assets/admin_user_list.html:115 +#: assets/templates/assets/admin_user_list.html:113 #: assets/templates/assets/asset_detail.html:31 #: assets/templates/assets/asset_list.html:170 #: assets/templates/assets/cmd_filter_detail.html:33 @@ -515,15 +515,14 @@ msgstr "创建远程应用" #: applications/templates/applications/user_remote_app_list.html:21 #: assets/models/cmd_filter.py:54 #: assets/templates/assets/_asset_user_list.html:20 -#: assets/templates/assets/admin_user_list.html:54 +#: assets/templates/assets/admin_user_list.html:52 #: assets/templates/assets/asset_list.html:100 #: assets/templates/assets/cmd_filter_list.html:28 #: assets/templates/assets/cmd_filter_rule_list.html:63 #: 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 "连接" @@ -598,42 +598,36 @@ msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" #: assets/const.py:77 assets/models/utils.py:43 -#: assets/templates/assets/admin_user_list.html:51 -#: assets/templates/assets/system_user_list.html:57 msgid "Unreachable" msgstr "不可达" #: assets/const.py:78 assets/models/utils.py:44 -#: assets/templates/assets/admin_user_list.html:50 #: assets/templates/assets/asset_list.html:99 -#: assets/templates/assets/system_user_list.html:56 -#: users/templates/users/user_group_granted_asset.html:47 msgid "Reachable" msgstr "可连接" #: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:9 -#: xpack/plugins/license/models.py:79 +#: xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" -#: assets/forms/asset.py:24 assets/models/asset.py:117 +#: 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:122 +#: 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:126 +#: 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 @@ -646,28 +640,28 @@ 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:121 +#: 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:255 +#: 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 -#: xpack/plugins/change_auth_plan/forms.py:115 +#: xpack/plugins/change_auth_plan/forms.py:116 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:55 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:15 #: xpack/plugins/cloud/models.py:123 @@ -695,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 -#: xpack/plugins/change_auth_plan/forms.py:105 +#: 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 "内容不能包含: {}" @@ -719,7 +713,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/templates/assets/_asset_user_auth_view_modal.html:21 #: assets/templates/assets/_asset_user_list.html:16 #: assets/templates/assets/admin_user_detail.html:60 -#: assets/templates/assets/admin_user_list.html:48 +#: assets/templates/assets/admin_user_list.html:46 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:52 audits/models.py:94 @@ -730,11 +724,11 @@ 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 -#: xpack/plugins/change_auth_plan/forms.py:99 +#: xpack/plugins/change_auth_plan/forms.py:100 #: xpack/plugins/change_auth_plan/models.py:63 #: xpack/plugins/change_auth_plan/models.py:409 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 @@ -769,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私钥" @@ -809,135 +803,129 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写" msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:112 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:113 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:116 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 -#: terminal/templates/terminal/session_list.html:75 msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:119 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:120 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:123 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:129 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:130 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:133 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:134 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:135 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:137 +#: assets/models/asset.py:184 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:138 +#: assets/models/asset.py:185 #: xpack/plugins/license/templates/license/license_detail.html:80 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:139 +#: assets/models/asset.py:186 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:140 +#: assets/models/asset.py:187 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:141 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:142 +#: assets/models/asset.py:189 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:143 +#: assets/models/asset.py:190 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:145 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:146 +#: assets/models/asset.py:193 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:147 +#: assets/models/asset.py:194 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:148 +#: assets/models/asset.py:195 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:150 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 "标签管理" @@ -981,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 "手机" @@ -1003,12 +991,11 @@ msgid "Operator" msgstr "运营商" #: assets/models/cluster.py:36 assets/models/group.py:34 -#: perms/utils/asset_permission.py:106 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 "系统" @@ -1115,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 @@ -1127,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 @@ -1136,7 +1123,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:246 +#: assets/models/label.py:19 assets/models/node.py:244 #: assets/templates/assets/label_list.html:15 settings/models.py:30 msgid "Value" msgstr "值" @@ -1145,11 +1132,11 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:245 +#: assets/models/node.py:243 msgid "Key" msgstr "键" -#: assets/models/node.py:303 +#: assets/models/node.py:301 msgid "New node" msgstr "新节点" @@ -1171,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 @@ -1208,12 +1195,6 @@ msgstr "登录模式" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/serializers/admin_user.py:36 assets/serializers/asset.py:64 -#: assets/serializers/asset_user.py:29 assets/serializers/system_user.py:31 -#: assets/templates/assets/_asset_user_list.html:18 -msgid "Connectivity" -msgstr "连接" - #: assets/serializers/asset.py:21 msgid "Protocol format should {}/{}" msgstr "协议格式 {}/{}" @@ -1222,6 +1203,11 @@ msgstr "协议格式 {}/{}" msgid "Protocol duplicate: {}" msgstr "协议重复: {}" +#: assets/serializers/asset.py:64 assets/serializers/asset_user.py:29 +#: assets/templates/assets/_asset_user_list.html:18 +msgid "Connectivity" +msgstr "连接" + #: assets/serializers/asset.py:90 msgid "Hardware info" msgstr "硬件信息" @@ -1235,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 @@ -1247,19 +1233,19 @@ msgstr "ssh公钥" msgid "private key invalid" msgstr "密钥不合法" -#: assets/serializers/node.py:33 +#: assets/serializers/node.py:32 msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:32 +#: assets/serializers/system_user.py:31 msgid "Login mode display" msgstr "登录模式显示" -#: assets/serializers/system_user.py:67 +#: assets/serializers/system_user.py:66 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:76 +#: assets/serializers/system_user.py:75 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -1370,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 "系统用户" @@ -1401,7 +1385,7 @@ msgid "Update asset user auth" msgstr "更新资产用户认证信息" #: assets/templates/assets/_asset_user_auth_update_modal.html:23 -#: xpack/plugins/change_auth_plan/forms.py:101 +#: xpack/plugins/change_auth_plan/forms.py:102 msgid "Please input password" msgstr "请输入密码" @@ -1490,19 +1474,19 @@ msgstr "重命名节点" msgid "Delete node" msgstr "删除节点" -#: assets/templates/assets/_node_tree.html:155 +#: assets/templates/assets/_node_tree.html:154 msgid "Create node failed" msgstr "创建节点失败" -#: assets/templates/assets/_node_tree.html:167 +#: assets/templates/assets/_node_tree.html:166 msgid "Have child node, cancel" msgstr "存在子节点,不能删除" -#: assets/templates/assets/_node_tree.html:169 +#: assets/templates/assets/_node_tree.html:168 msgid "Have assets, cancel" msgstr "存在资产,不能删除" -#: assets/templates/assets/_node_tree.html:243 +#: assets/templates/assets/_node_tree.html:242 msgid "Rename success" msgstr "重命名成功" @@ -1581,15 +1565,15 @@ 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 -#: xpack/plugins/change_auth_plan/forms.py:109 +#: 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" msgstr "选择节点" #: assets/templates/assets/admin_user_detail.html:100 #: assets/templates/assets/asset_detail.html:207 -#: assets/templates/assets/asset_list.html:396 +#: assets/templates/assets/asset_list.html:395 #: assets/templates/assets/cmd_filter_detail.html:106 #: assets/templates/assets/system_user_assets.html:100 #: assets/templates/assets/system_user_detail.html:182 @@ -1611,24 +1595,24 @@ msgstr "选择节点" msgid "Confirm" msgstr "确认" -#: assets/templates/assets/admin_user_list.html:7 +#: assets/templates/assets/admin_user_list.html:5 msgid "" "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL " "sudo permissions users, " msgstr "" "管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户," -#: assets/templates/assets/admin_user_list.html:8 +#: assets/templates/assets/admin_user_list.html:6 msgid "" "Jumpserver users of the system using the user to `push system user`, `get " "assets hardware information`, etc. " msgstr "Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。" -#: assets/templates/assets/admin_user_list.html:9 +#: assets/templates/assets/admin_user_list.html:7 msgid "You can set any one for Windows or other hardware." msgstr "Windows或其它硬件可以随意设置一个" -#: assets/templates/assets/admin_user_list.html:19 +#: assets/templates/assets/admin_user_list.html:17 #: assets/templates/assets/asset_list.html:68 #: assets/templates/assets/system_user_list.html:23 #: audits/templates/audits/login_log_list.html:85 @@ -1638,7 +1622,7 @@ msgstr "Windows或其它硬件可以随意设置一个" msgid "Export" msgstr "导出" -#: assets/templates/assets/admin_user_list.html:24 +#: assets/templates/assets/admin_user_list.html:22 #: assets/templates/assets/asset_list.html:73 #: assets/templates/assets/system_user_list.html:28 #: settings/templates/settings/_ldap_list_users_modal.html:100 @@ -1649,20 +1633,13 @@ msgstr "导出" msgid "Import" msgstr "导入" -#: assets/templates/assets/admin_user_list.html:39 +#: assets/templates/assets/admin_user_list.html:37 #: assets/views/admin_user.py:50 msgid "Create admin user" msgstr "创建管理用户" -#: assets/templates/assets/admin_user_list.html:52 -#: assets/templates/assets/system_user_list.html:58 -#: ops/templates/ops/adhoc_history.html:54 -#: ops/templates/ops/task_history.html:60 -msgid "Ratio" -msgstr "比例" - -#: assets/templates/assets/admin_user_list.html:165 -#: assets/templates/assets/admin_user_list.html:196 +#: assets/templates/assets/admin_user_list.html:163 +#: assets/templates/assets/admin_user_list.html:194 #: assets/templates/assets/asset_list.html:268 #: assets/templates/assets/asset_list.html:305 #: assets/templates/assets/system_user_list.html:225 @@ -1719,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 @@ -1728,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 "激活中" @@ -1807,7 +1781,7 @@ msgstr "仅显示当前节点资产" msgid "Displays all child node assets" msgstr "显示所有子节点资产" -#: assets/templates/assets/asset_list.html:390 +#: assets/templates/assets/asset_list.html:389 #: assets/templates/assets/system_user_list.html:166 #: users/templates/users/user_detail.html:382 #: users/templates/users/user_detail.html:408 @@ -1818,11 +1792,11 @@ msgstr "显示所有子节点资产" msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_list.html:391 +#: assets/templates/assets/asset_list.html:390 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" -#: assets/templates/assets/asset_list.html:394 +#: assets/templates/assets/asset_list.html:393 #: assets/templates/assets/system_user_list.html:170 #: settings/templates/settings/terminal_setting.html:166 #: users/templates/users/user_detail.html:386 @@ -1836,16 +1810,16 @@ msgstr "删除选择资产" msgid "Cancel" msgstr "取消" -#: assets/templates/assets/asset_list.html:407 +#: assets/templates/assets/asset_list.html:406 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:408 -#: assets/templates/assets/asset_list.html:412 +#: assets/templates/assets/asset_list.html:407 +#: assets/templates/assets/asset_list.html:411 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:411 +#: assets/templates/assets/asset_list.html:410 msgid "Asset Deleting failed." msgstr "删除失败" @@ -1895,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 "命令过滤器规则列表" @@ -1922,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 "创建网关" @@ -2070,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 "更新命令过滤器规则" @@ -2090,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 "创建网关" @@ -2110,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 "更新标签" @@ -2232,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" @@ -2445,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开源堡垒机" @@ -2650,7 +2624,7 @@ msgstr "不能包含特殊字符" msgid "This field must be unique." msgstr "字段必须唯一" -#: jumpserver/views.py:190 +#: jumpserver/views.py:191 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, coco, " "configure nginx for url distribution,
If you see this page, " @@ -2853,6 +2827,11 @@ msgstr "执行历史" msgid "F/S/T" msgstr "失败/成功/总" +#: ops/templates/ops/adhoc_history.html:54 +#: ops/templates/ops/task_history.html:60 +msgid "Ratio" +msgstr "比例" + #: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:142 msgid "Run history detail" msgstr "执行历史详情" @@ -3005,63 +2984,71 @@ msgstr "命令执行" msgid "Organization" 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/api/mixin.py:128 +msgid "ungrouped" +msgstr "未分组" + +#: perms/api/mixin.py:133 +msgid "empty" +msgstr "空" + +#: 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 "失效日期" @@ -3088,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 @@ -3108,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" @@ -3207,19 +3194,15 @@ msgstr "添加用户" msgid "Add user group to this permission" msgstr "添加用户组" -#: perms/utils/asset_permission.py:115 -msgid "Empty" -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 "权限管理" @@ -3244,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 "资产授权资产列表" @@ -3256,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 "远程应用授权远程应用列表" @@ -3471,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 " @@ -3509,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)" @@ -3601,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 "邮件" @@ -3935,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 @@ -4389,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 @@ -4417,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密钥不合法" @@ -4502,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是初始的超级管理员" @@ -4588,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 "密码不满足安全规则" @@ -4633,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 "更新用户组" @@ -4720,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" @@ -4738,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" @@ -4885,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 "用户组详情" @@ -5227,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 "用户组授权资产" @@ -5313,25 +5306,28 @@ msgid "Password length" msgstr "密码长度" #: xpack/plugins/change_auth_plan/forms.py:45 -msgid "* For security, please do not change root user's password" -msgstr "* 为了安全,请不要更改root用户的密码" +#: xpack/plugins/change_auth_plan/models.py:213 +#, fuzzy +#| msgid "For security, do not change {} user's password" +msgid "* For security, do not change {} user's password" +msgstr "* 为了安全,禁止更改 {} 用户的密码" -#: xpack/plugins/change_auth_plan/forms.py:54 +#: xpack/plugins/change_auth_plan/forms.py:55 msgid "* Please enter custom password" msgstr "* 请输入自定义密码" -#: xpack/plugins/change_auth_plan/forms.py:63 +#: xpack/plugins/change_auth_plan/forms.py:64 msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" -#: xpack/plugins/change_auth_plan/forms.py:116 +#: xpack/plugins/change_auth_plan/forms.py:117 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:60 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:81 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 msgid "Periodic perform" msgstr "定时执行" -#: xpack/plugins/change_auth_plan/forms.py:120 +#: xpack/plugins/change_auth_plan/forms.py:121 msgid "" "Tips: The username of the user on the asset to be modified. if the user " "exists, change the password; If the user does not exist, create the user." @@ -5339,11 +5335,11 @@ msgstr "" "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如果" "用户不存在,则创建用户。" -#: xpack/plugins/change_auth_plan/forms.py:124 +#: xpack/plugins/change_auth_plan/forms.py:125 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" -#: xpack/plugins/change_auth_plan/forms.py:125 +#: xpack/plugins/change_auth_plan/forms.py:126 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions (= 0 - asset_has_matched = False - for asset, system_users in assets.items(): - asset_matched = (asset.hostname.lower().find(keyword.lower()) >= 0) \ - or (asset.ip.find(keyword.lower()) >= 0) - if node_matched or asset_matched: - asset_has_matched = True - fake_node = asset.as_node() - fake_node.assets_amount = 0 - system_users = [s for s in system_users if - asset.has_protocol(s.protocol)] - fake_node.asset.system_users_granted = system_users - fake_node.key = node.key + ':0' - matched_assets.append(fake_node) - if asset_has_matched: - node.assets_amount = len(matched_assets) - queryset.append(node) - queryset.extend(sorted(matched_assets, key=lambda x: x.value)) - return queryset + _nodes = [] + for n in nodes_with_assets: + key = n["key"] + node = nodes_map.get(key) + node._assets_amount = n["assets_amount"] + _nodes.append(node) + return _nodes + + def get_serializer(self, nodes_with_assets, many=True): + nodes = self.get_nodes(nodes_with_assets) + return super().get_serializer(nodes, many=True) def get_queryset(self): - keyword = self.request.query_params.get('search') - if keyword: - return self.get_search_queryset(keyword) + user = self.get_object() + self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy) + nodes_with_assets = self.util.get_nodes_with_assets() + return nodes_with_assets + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() + + +class UserGrantedNodesAsTreeApi(UserGrantedNodesApi): + serializer_class = TreeNodeSerializer + only_fields = ParserNode.nodes_only_fields + + def get_serializer(self, nodes_with_assets, many=True): + nodes = self.get_nodes(nodes_with_assets) + queryset = [] + for node in nodes: + data = ParserNode.parse_node_to_tree_node(node) + queryset.append(data) + return self.get_serializer_class()(queryset, many=many) + + +class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, NodesWithUngroupMixin, ListAPIView): + """ + 用户授权的节点并带着节点下资产的api + """ + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.NodeGrantedSerializer + pagination_class = LimitOffsetPagination + + nodes_only_fields = serializers.NodeGrantedSerializer.Meta.only_fields + assets_only_fields = serializers.NodeGrantedSerializer.assets_only_fields + system_users_only_fields = serializers.NodeGrantedSerializer.system_users_only_fields + + def get_object(self): + user_id = self.kwargs.get('pk', '') + if not user_id: + user = self.request.user else: - return self.get_children_queryset() + user = get_object_or_404(User, id=user_id) + return user + + def get_maps(self, nodes_items): + """ + 查库,并加入构造的ungrouped节点 + :return: + ({asset.id: asset}, {node.key: node}, {system_user.id: system_user}) + """ + _nodes_keys = set() + _assets_ids = set() + _system_users_ids = set() + for item in nodes_items: + _nodes_keys.add(item["key"]) + _assets_ids.update(set(item["assets"].keys())) + for _system_users_id in item["assets"].values(): + _system_users_ids.update(_system_users_id.keys()) + + _nodes = Node.objects.filter(key__in=_nodes_keys).only( + *self.nodes_only_fields + ) + _assets = Asset.objects.filter(id__in=_assets_ids).only( + *self.assets_only_fields + ) + _system_users = SystemUser.objects.filter(id__in=_system_users_ids).only( + *self.system_users_only_fields + ) + _nodes_map = {n.key: n for n in _nodes} + self.add_ungrouped_nodes(_nodes_map, _nodes_keys) + _assets_map = {a.id: a for a in _assets} + _system_users_map = {s.id: s for s in _system_users} + return _nodes_map, _assets_map, _system_users_map + + def get_serializer_queryset(self, nodes_items): + """ + 将id转为object,同时构造queryset + :param nodes_items: + [ + { + 'key': node.key, + 'assets_amount': 10 + 'assets': { + asset.id: { + system_user.id: actions, + }, + }, + }, + ] + """ + queryset = [] + _node_map, _assets_map, _system_users_map = self.get_maps(nodes_items) + for item in nodes_items: + key = item["key"] + node = _node_map.get(key) + if not node: + continue + node._assets_amount = item["assets_amount"] + assets_granted = [] + for asset_id, system_users_ids_action in item["assets"].items(): + asset = _assets_map.get(asset_id) + if not asset: + continue + system_user_granted = [] + for system_user_id, action in system_users_ids_action.items(): + system_user = _system_users_map.get(system_user_id) + if not system_user: + continue + system_user.actions = action + system_user_granted.append(system_user) + asset.system_users_granted = system_user_granted + assets_granted.append(asset) + node.assets_granted = assets_granted + queryset.append(node) + return queryset + + def get_serializer(self, nodes_items, many=True): + queryset = self.get_serializer_queryset(nodes_items) + return super().get_serializer(queryset, many=many) + + def get_queryset(self): + user = self.get_object() + self.util = AssetPermissionUtil(user, cache_policy=self.cache_policy) + system_user_id = self.request.query_params.get('system_user') + if system_user_id: + self.util.filter_permissions( + system_users=system_user_id + ) + nodes_items = self.util.get_nodes_with_assets() + return nodes_items + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() + + +class UserGrantedNodesWithAssetsAsTreeApi(UserGrantedNodesWithAssetsApi): + serializer_class = TreeNodeSerializer + permission_classes = (IsOrgAdminOrAppUser,) + system_user_id = None + nodes_only_fields = ParserNode.nodes_only_fields + assets_only_fields = ParserNode.assets_only_fields + system_users_only_fields = ParserNode.system_users_only_fields + + def get_serializer(self, nodes_items, many=True): + _queryset = super().get_serializer_queryset(nodes_items) + queryset = [] + + for node in _queryset: + data = ParserNode.parse_node_to_tree_node(node) + queryset.append(data) + for asset in node.assets_granted: + system_users = asset.system_users_granted + data = ParserNode.parse_asset_to_tree_node(node, asset, system_users) + queryset.append(data) + queryset = sorted(queryset) + return self.serializer_class(queryset, many=True) class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView): @@ -412,24 +316,24 @@ class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView): asset_id = request.query_params.get('asset_id', '') system_id = request.query_params.get('system_user_id', '') action_name = request.query_params.get('action_name', '') + cache_policy = self.request.query_params.get("cache_policy", '0') + + try: + asset_id = uuid.UUID(asset_id) + system_id = uuid.UUID(system_id) + except ValueError: + return Response({'msg': False}, status=403) 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, {}) - - if su not in granted_system_users: - return Response({'msg': False}, status=403) - - action = granted_system_users[su] - choices = Action.value_to_choices(action) - if action_name not in choices: - return Response({'msg': False}, status=403) - - return Response({'msg': True}, status=200) + util = AssetPermissionUtil(user, cache_policy=cache_policy) + assets = util.get_assets() + for asset in assets: + if asset_id == asset["id"]: + action = asset["system_users"].get(system_id) + if action and action_name in Action.value_to_choices(action): + return Response({'msg': True}, status=200) + break + return Response({'msg': False}, status=403) class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, RetrieveAPIView): @@ -442,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/api/user_remote_app_permission.py b/apps/perms/api/user_remote_app_permission.py index 4cbfcd9b4..5c816cffb 100644 --- a/apps/perms/api/user_remote_app_permission.py +++ b/apps/perms/api/user_remote_app_permission.py @@ -13,13 +13,13 @@ from ..utils import ( RemoteAppPermissionUtil, construct_remote_apps_tree_root, parse_remote_app_to_tree_node, ) -from ..hands import User, RemoteApp, RemoteAppSerializer +from ..hands import User, RemoteApp, RemoteAppSerializer, UserGroup from ..mixins import RemoteAppFilterMixin __all__ = [ 'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi', - 'UserGrantedRemoteAppsAsTreeApi', + 'UserGrantedRemoteAppsAsTreeApi', 'UserGroupGrantedRemoteAppsApi', ] @@ -94,3 +94,20 @@ class ValidateUserRemoteAppPermissionApi(APIView): if remote_app not in remote_apps: return Response({'msg': False}, status=403) return Response({'msg': True}, status=200) + + +# RemoteApp permission + +class UserGroupGrantedRemoteAppsApi(ListAPIView): + permission_classes = (IsOrgAdminOrAppUser, ) + serializer_class = RemoteAppSerializer + + def get_queryset(self): + queryset = [] + user_group_id = self.kwargs.get('pk') + if not user_group_id: + return queryset + user_group = get_object_or_404(UserGroup, id=user_group_id) + util = RemoteAppPermissionUtil(user_group) + queryset = util.get_remote_apps() + return queryset diff --git a/apps/perms/const.py b/apps/perms/const.py index b18580747..4ccab5e38 100644 --- a/apps/perms/const.py +++ b/apps/perms/const.py @@ -3,3 +3,4 @@ UNGROUPED_NODE_ID = "00000000-0000-0000-0000-000000000002" EMPTY_NODE_ID = "00000000-0000-0000-0000-000000000003" +EMPTY_NODE_KEY = "1:-2" 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 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/models/asset_permission.py b/apps/perms/models/asset_permission.py index d5fafc63a..b10d33d6b 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -2,10 +2,12 @@ import uuid from functools import reduce from django.db import models +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from common.utils import date_expired_default, set_or_append_attr_bulk from orgs.mixins import OrgModelMixin +from assets.models import Asset, SystemUser, Node from .base import BasePermission @@ -85,14 +87,23 @@ class AssetPermission(BasePermission): @classmethod def get_queryset_with_prefetch(cls): - return cls.objects.all().valid().prefetch_related('nodes', 'assets', 'system_users') + return cls.objects.all().valid().prefetch_related( + models.Prefetch('nodes', queryset=Node.objects.all().only('key')), + models.Prefetch('assets', queryset=Asset.objects.all().only('id')), + models.Prefetch('system_users', queryset=SystemUser.objects.all().only('id')) + ) def get_all_assets(self): - assets = set(self.assets.all()) - for node in self.nodes.all(): - _assets = node.get_all_assets() - set_or_append_attr_bulk(_assets, 'inherit', node.value) - assets.update(set(_assets)) + args = [Q(granted_by_permissions=self)] + pattern = set() + nodes_keys = self.nodes.all().values_list('key', flat=True) + for key in nodes_keys: + pattern.add(r'^{0}$|^{0}:'.format(key)) + pattern = '|'.join(list(pattern)) + if pattern: + args.append(Q(nodes__key__regex=pattern)) + args = reduce(lambda x, y: x | y, args) + assets = Asset.objects.filter(args) return assets diff --git a/apps/perms/serializers/asset_permission.py b/apps/perms/serializers/asset_permission.py index ecbed669b..7bc145d9f 100644 --- a/apps/perms/serializers/asset_permission.py +++ b/apps/perms/serializers/asset_permission.py @@ -6,11 +6,12 @@ from rest_framework import serializers from common.fields import StringManyToManyField from orgs.mixins import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action +from assets.models import Asset __all__ = [ 'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer', 'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer', - 'ActionsField', + 'ActionsField', 'AssetPermissionAssetsSerializer', ] @@ -70,3 +71,11 @@ class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer): class Meta: model = AssetPermission fields = ['id', 'assets'] + + +class AssetPermissionAssetsSerializer(serializers.ModelSerializer): + + class Meta: + model = Asset + only_fields = ['id', 'hostname', 'ip'] + fields = tuple(only_fields) diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 639ba7140..bdf2e1fdc 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -2,16 +2,16 @@ # from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ -from assets.models import Node, SystemUser -from assets.serializers import AssetSerializer - +from assets.models import Node, SystemUser, Asset +from assets.serializers import ProtocolsField from .asset_permission import ActionsField __all__ = [ - 'AssetPermissionNodeSerializer', 'GrantedNodeSerializer', + 'GrantedNodeSerializer', 'NodeGrantedSerializer', 'AssetGrantedSerializer', - 'ActionsSerializer', + 'ActionsSerializer', 'AssetSystemUserSerializer', ] @@ -23,87 +23,56 @@ class AssetSystemUserSerializer(serializers.ModelSerializer): class Meta: model = SystemUser - fields = ( - 'id', 'name', 'username', 'priority', "actions", + only_fields = ( + 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', ) + fields = list(only_fields) + ["actions"] + read_only_fields = fields -class AssetGrantedSerializer(AssetSerializer): +class AssetGrantedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 """ + protocols = ProtocolsField(label=_('Protocols'), required=False, read_only=True) system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) system_users_join = serializers.SerializerMethodField() + system_users_only_fields = AssetSystemUserSerializer.Meta.only_fields + + class Meta: + model = Asset + only_fields = [ + "id", "hostname", "ip", "protocols", "os", 'domain', + "platform", "org_id", + ] + fields = only_fields + ['system_users_granted', 'system_users_join', "org_name"] + read_only_fields = fields @staticmethod def get_system_users_join(obj): system_users = [s.username for s in obj.system_users_granted] return ', '.join(system_users) - def get_field_names(self, declared_fields, info): - fields = ( - "id", "hostname", "ip", "protocols", - "system_users_granted", "is_active", "system_users_join", "os", - 'domain', "platform", "comment", "org_id", "org_name", - ) - return fields - - -class AssetPermissionNodeSerializer(serializers.ModelSerializer): - asset = AssetGrantedSerializer(required=False) - assets_amount = serializers.SerializerMethodField() - - tree_id = serializers.SerializerMethodField() - tree_parent = serializers.SerializerMethodField() - - class Meta: - model = Node - fields = [ - 'id', 'key', 'value', 'asset', 'is_node', 'org_id', - 'tree_id', 'tree_parent', 'assets_amount', - ] - - @staticmethod - def get_assets_amount(obj): - return obj.assets_amount - - @staticmethod - def get_tree_id(obj): - return obj.key - - @staticmethod - def get_tree_parent(obj): - return obj.parent_key - class NodeGrantedSerializer(serializers.ModelSerializer): """ 授权资产组 """ assets_granted = AssetGrantedSerializer(many=True, read_only=True) - assets_amount = serializers.SerializerMethodField() - parent = serializers.SerializerMethodField() - name = serializers.SerializerMethodField() + assets_amount = serializers.ReadOnlyField() + name = serializers.ReadOnlyField(source='value') + + assets_only_fields = AssetGrantedSerializer.Meta.only_fields + system_users_only_fields = AssetGrantedSerializer.system_users_only_fields class Meta: model = Node - fields = [ - 'id', 'key', 'name', 'value', 'parent', - 'assets_granted', 'assets_amount', 'org_id', + only_fields = ['id', 'key', 'value', "org_id"] + fields = only_fields + [ + 'name', 'assets_granted', 'assets_amount', ] - - @staticmethod - def get_assets_amount(obj): - return len(obj.assets_granted) - - @staticmethod - def get_name(obj): - return obj.name - - @staticmethod - def get_parent(obj): - return obj.parent.id + read_only_fields = fields class GrantedNodeSerializer(serializers.ModelSerializer): @@ -112,6 +81,7 @@ class GrantedNodeSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', 'key', 'value', ] + read_only_fields = fields class ActionsSerializer(serializers.Serializer): diff --git a/apps/perms/templates/perms/asset_permission_asset.html b/apps/perms/templates/perms/asset_permission_asset.html index e774692f3..d8cd8ee96 100644 --- a/apps/perms/templates/perms/asset_permission_asset.html +++ b/apps/perms/templates/perms/asset_permission_asset.html @@ -48,29 +48,19 @@
- +
- - - - - + + + + + - {% 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/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 611661c5d..093692a15 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -1,7 +1,8 @@ # coding:utf-8 -from django.urls import path +from django.urls import path, re_path from rest_framework import routers +from common import api as capi from .. import api app_name = 'perms' @@ -12,26 +13,37 @@ router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remot asset_permission_urlpatterns = [ - # 查询某个用户授权的资产和资产组 - path('user//assets/', api.UserGrantedAssetsApi.as_view()), + # Assets path('users//assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'), - path('user/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'), - path('user//nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), - path('user/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), - path('user/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(), name='my-node-children'), - path('user//nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), - path('user/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'), - path('user//nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'), - path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='my-nodes-assets'), - path('user//nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'), - path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-assets-as-tree'), + path('users/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'), + + # Node as tree + path('users//nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), + path('users/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), + + # Nodes + path('users//nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), + path('users/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), + + # Node assets + path('users//nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), + path('users/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'), + + # Node with assets + path('users//nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'), + path('users/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='my-nodes-assets'), + + # Node assets as tree + path('users//nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'), + path('users/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-assets-as-tree'), # 查询某个用户组授权的资产和资产组 - path('user-group//assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), - path('user-group//nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), - path('user-group//nodes-assets/', api.UserGroupGrantedNodesWithAssetsApi.as_view(), name='user-group-nodes-assets'), - path('user-group//nodes-assets/tree/', api.UserGroupGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-group-nodes-assets-as-tree'), - path('user-group//nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), + path('user-groups//assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-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'), # 用户和资产授权变更 path('asset-permissions//user/remove/', api.AssetPermissionRemoveUserApi.as_view(), name='asset-permission-remove-user'), @@ -39,26 +51,29 @@ asset_permission_urlpatterns = [ path('asset-permissions//asset/remove/', api.AssetPermissionRemoveAssetApi.as_view(), name='asset-permission-remove-asset'), path('asset-permissions//asset/add/', api.AssetPermissionAddAssetApi.as_view(), name='asset-permission-add-asset'), + # 授权规则中授权的资产 + path('asset-permissions//assets/', api.AssetPermissionAssetsApi.as_view(), name='asset-permission-assets'), + # 验证用户是否有某个资产和系统用户的权限 - path('asset-permission/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), - path('asset-permission/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), + path('asset-permissions/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), + path('asset-permissions/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), ] remote_app_permission_urlpatterns = [ # 查询用户授权的RemoteApp - path('user//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), - path('user/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), + path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), + path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), # 获取用户授权的RemoteApp树 - path('user//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), - path('user/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), + path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), + path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), # 查询用户组授权的RemoteApp - path('user-group//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), + path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), # 校验用户对RemoteApp的权限 - path('remote-app-permission/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), + path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), # 用户和RemoteApp变更 path('remote-app-permissions//user/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'), @@ -67,7 +82,11 @@ remote_app_permission_urlpatterns = [ path('remote-app-permissions//remote-app/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), ] -urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns +old_version_urlpatterns = [ + re_path('(?Puser|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api) +] + +urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns + old_version_urlpatterns urlpatterns += router.urls diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 3a8c2d387..c437753b6 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -1,80 +1,65 @@ # coding: utf-8 +import time import uuid from collections import defaultdict import json from hashlib import md5 -import time import itertools from django.utils import timezone from django.db.models import Q from django.core.cache import cache from django.conf import settings -from django.utils.translation import ugettext as _ from orgs.utils import set_to_root_org -from common.utils import get_logger +from common.utils import get_logger, timeit from common.tree import TreeNode +from assets.utils import NodeUtil from .. import const from ..models import AssetPermission, Action from ..hands import Node, Asset -from assets.utils import NodeUtil +from .stack import PermSystemUserNodeUtil, PermAssetsAmountUtil logger = get_logger(__file__) __all__ = [ 'AssetPermissionUtil', 'is_obj_attr_has', 'sort_assets', - 'parse_asset_to_tree_node', 'parse_node_to_tree_node', + 'ParserNode', ] -class TreeNodeCounter(NodeUtil): - def __init__(self, nodes): - self.__nodes = nodes - super().__init__(with_assets_amount=True) - - def get_queryset(self): - return self.__nodes - - -def timeit(func): - def wrapper(*args, **kwargs): - logger.debug("Start call: {}".format(func.__name__)) - now = time.time() - result = func(*args, **kwargs) - using = time.time() - now - logger.debug("Call {} end, using: {:.2}s".format(func.__name__, using)) - return result - return wrapper - - class GenerateTree: def __init__(self): """ nodes = { - "": { + node.key: { "system_users": { - "system_user": action, - "system_user2": action, + system_user.id: actions, }, - "assets": set([]), - } + "assets": set([asset.id,]), + }, } assets = { - "": { - "system_user": action, - "system_user2": action, + asset.id: { + system_user.id: actions, }, } """ self._node_util = None - self.nodes = defaultdict(lambda: {"system_users": defaultdict(int), "assets": set(), "assets_amount": 0}) + self.nodes = defaultdict(lambda: { + "system_users": defaultdict(int), "assets": set(), + "assets_amount": 0, "all_assets": set(), + }) self.assets = defaultdict(lambda: defaultdict(int)) self._root_node = None self._ungroup_node = None self._nodes_with_assets = None + self._all_assets_nodes_key = None + self._asset_counter = 0 + self._system_user_counter = 0 + self._nodes_assets_counter = 0 @property def node_util(self): @@ -82,115 +67,160 @@ class GenerateTree: self._node_util = NodeUtil() return self._node_util + @staticmethod + def key_sort(key): + key_list = [int(i) for i in key.split(':')] + return len(key_list), key_list + @property - def root_node(self): + def root_key(self): if self._root_node: return self._root_node - all_nodes = self.nodes.keys() + all_keys = self.nodes.keys() # 如果没有授权节点,就放到默认的根节点下 - if not all_nodes: + if not all_keys: return None - root_node = min(all_nodes) - self._root_node = root_node - return root_node + root_key = min(all_keys, key=self.key_sort) + self._root_key = root_key + return root_key @property - def ungrouped_node(self): + def all_assets_nodes_keys(self): + if not self._all_assets_nodes_key: + self._all_assets_nodes_key = Asset.get_all_nodes_keys() + return self._all_assets_nodes_key + + @property + def ungrouped_key(self): if self._ungroup_node: return self._ungroup_node - node_id = const.UNGROUPED_NODE_ID - if self.root_node: - node_key = "{}:{}".format(self.root_node.key, self.root_node.child_mark) + if self.root_key: + node_key = "{}:{}".format(self.root_key, '-1') else: - node_key = '0:0' - node_value = _("Default") - node = Node(id=node_id, key=node_key, value=node_value) - self.add_node(node, {}) - self._ungroup_node = node - return node + node_key = '1:-1' + self._ungroup_node = node_key + return node_key - @property - def empty_node(self): - node_id = const.EMPTY_NODE_ID - value = _('Empty') - node = Node(id=node_id, value=value) - return node + @timeit + def add_assets_without_system_users(self, assets_ids): + for asset_id in assets_ids: + self.add_asset(asset_id, {}) - #@timeit - def add_assets_without_system_users(self, assets): - for asset in assets: - self.add_asset(asset, {}) + @timeit + def add_assets(self, assets_ids_with_system_users): + for asset_id, system_users_ids in assets_ids_with_system_users.items(): + self.add_asset(asset_id, system_users_ids) - #@timeit - def add_assets(self, assets): - for asset, system_users in assets.items(): - self.add_asset(asset, system_users) + # @timeit + def add_asset(self, asset_id, system_users_ids=None): + """ + :param asset_id: + :param system_users_ids: {system_user.id: actions, } + :return: + """ + if not system_users_ids: + system_users_ids = defaultdict(int) - # #@timeit - def add_asset(self, asset, system_users=None): - nodes = asset.nodes.all() - nodes = self.node_util.get_nodes_by_queryset(nodes) - if not system_users: - system_users = defaultdict(int) - else: - system_users = {k: v for k, v in system_users.items()} - _system_users = self.assets[asset] - for system_user, action in _system_users.items(): - system_users[system_user] |= action + # 获取已有资产的系统用户和actions,并更新到最新系统用户信息中 + old_system_users_ids = self.assets[asset_id] + for system_user_id, action in old_system_users_ids.items(): + system_users_ids[system_user_id] |= action - # 获取父节点们 - parents = self.node_util.get_nodes_parents(nodes, with_self=True) - for node in parents: - _system_users = self.nodes[node]["system_users"] - self.nodes[node]["assets_amount"] += 1 - for system_user, action in _system_users.items(): - system_users[system_user] |= action - - # 过滤系统用户的协议 - system_users = {s: v for s, v in system_users.items() if asset.has_protocol(s.protocol)} - self.assets[asset] = system_users - - in_nodes = set(self.nodes.keys()) & set(nodes) + asset_nodes_keys = self.all_assets_nodes_keys.get(asset_id, []) + # {asset.id: [node.key, ], } + # 获取用户在的节点 + in_nodes = set(self.nodes.keys()) & set(asset_nodes_keys) if not in_nodes: - self.nodes[self.ungrouped_node]["assets_amount"] += 1 - self.nodes[self.ungrouped_node]["assets"].add(system_users) + self.nodes[self.ungrouped_key]["assets"].add(asset_id) + self.assets[asset_id] = system_users_ids return - for node in in_nodes: - self.nodes[node]["assets"].add(asset) + # 遍历用户应该在的节点 + for key in in_nodes: + # 把自己加入到树上的节点中 + self.nodes[key]["assets"].add(asset_id) + # 获取自己所在节点的系统用户,并添加进去 + node_system_users_ids = self.nodes[key]["system_users"] + for system_user_id, action in node_system_users_ids.items(): + system_users_ids[system_user_id] |= action + self.assets[asset_id] = system_users_ids - def add_node(self, node, system_users=None): - if not system_users: - system_users = defaultdict(int) - self.nodes[node]["system_users"] = system_users + def add_node(self, node_key, system_users_ids=None): + """ + :param node_key: node.key + :param system_users_ids: {system_user.id: actions,} + :return: + """ + if not system_users_ids: + system_users_ids = defaultdict(int) + self.nodes[node_key]["system_users"] = system_users_ids # 添加树节点 - #@timeit - def add_nodes(self, nodes): - _nodes = nodes.keys() - family = self.node_util.get_family(_nodes, with_children=True) - for node in family: - self.add_node(node, nodes.get(node, {})) + @timeit + def add_nodes(self, nodes_keys_with_system_users_ids): + """ + :param nodes_keys_with_system_users_ids: + {node.key: {system_user.id: actions,}, } + :return: + """ + util = PermSystemUserNodeUtil() + family = util.get_nodes_family_and_system_users(nodes_keys_with_system_users_ids) + for key, system_users in family.items(): + self.add_node(key, system_users) def get_assets(self): - return dict(self.assets) + """ + :return: + [ + {"id": asset.id, "system_users": {system_user.id: actions, }}, + ] + """ + assets = [] + for asset_id, system_users in self.assets.items(): + assets.append({"id": asset_id, "system_users": system_users}) + return assets - #@timeit + @timeit def get_nodes_with_assets(self): + """ + :return: + [ + { + 'key': node.key, + 'assets_amount': 10 + 'assets': { + asset.id: { + system_user.id: actions, + }, + }, + }, + ] + """ if self._nodes_with_assets: return self._nodes_with_assets - nodes = {} - for node, values in self.nodes.items(): - node._assets_amount = values["assets_amount"] - nodes[node] = {asset: self.assets.get(asset, {}) for asset in values["assets"]} + util = PermAssetsAmountUtil() + nodes_with_assets_amount = util.compute_nodes_assets_amount(self.nodes) + nodes = [] + for key, values in nodes_with_assets_amount.items(): + assets = {asset_id: self.assets.get(asset_id) for asset_id in values["assets"]} + nodes.append({ + "key": key, "assets": assets, + "assets_amount": values["assets_amount"] + }) # 如果返回空节点,页面构造授权资产树报错 if not nodes: - nodes[self.empty_node] = {} + nodes.append({ + "key": const.EMPTY_NODE_KEY, "assets": {}, "assets_amount": 0 + }) + nodes.sort(key=lambda n: self.key_sort(n["key"])) self._nodes_with_assets = nodes - return dict(nodes) + return nodes def get_nodes(self): - return list(self.nodes.keys()) + nodes = list(self.nodes.keys()) + if not nodes: + nodes.append(const.EMPTY_NODE_KEY) + return list(nodes) def get_user_permissions(user, include_group=True): @@ -228,8 +258,8 @@ def get_system_user_permissions(system_user): class AssetPermissionCacheMixin: - CACHE_KEY_PREFIX = '_ASSET_PERM_CACHE_' - CACHE_META_KEY_PREFIX = '_ASSET_PERM_META_KEY_' + CACHE_KEY_PREFIX = '_ASSET_PERM_CACHE_V2_' + CACHE_META_KEY_PREFIX = '_ASSET_PERM_META_KEY_V2_' CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh')) cache_policy = '1' @@ -283,6 +313,7 @@ class AssetPermissionCacheMixin: return self.get_cache_key('SYSTEM_USER') def get_resource_from_cache(self, resource): + logger.debug("Try get resource from cache") key_map = { "assets": self.asset_key, "nodes": self.node_key, @@ -294,18 +325,22 @@ class AssetPermissionCacheMixin: raise ValueError("Not a valid resource: {}".format(resource)) cached = cache.get(key) if not cached: + logger.debug("Not found resource cache, update it") self.update_cache() cached = cache.get(key) return cached def get_resource(self, resource): if self._is_using_cache(): + logger.debug("Using cache to get resource") return self.get_resource_from_cache(resource) elif self._is_refresh_cache(): + logger.debug("Need refresh cache") self.expire_cache() data = self.get_resource_from_cache(resource) return data else: + logger.debug("Not using cache get source") return self.get_resource_without_cache(resource) def get_resource_without_cache(self, resource): @@ -430,88 +465,91 @@ class AssetPermissionUtil(AssetPermissionCacheMixin): self._permissions = permissions return permissions - #@timeit + @timeit def filter_permissions(self, **filters): filters_json = json.dumps(filters, sort_keys=True) self._permissions = self.permissions.filter(**filters) self._filter_id = md5(filters_json.encode()).hexdigest() - #@timeit + @timeit def get_nodes_direct(self): """ - 返回用户/组授权规则直接关联的节点 - :return: {node1: {system_user1: {'actions': set()},}} + 返回直接授权的节点, + 并将节点添加到tree.nodes中,并将节点下的资产添加到tree.assets中 + :return: + {node.key: {system_user.id: actions,}, } """ if self._nodes_direct: return self._nodes_direct - nodes = defaultdict(lambda: defaultdict(int)) + nodes_keys = defaultdict(lambda: defaultdict(int)) for perm in self.permissions: actions = [perm.actions] - system_users = perm.system_users.all() - _nodes = perm.nodes.all() - for node, system_user, action in itertools.product(_nodes, system_users, actions): - nodes[node][system_user] |= action - self.tree.add_nodes(nodes) - self._nodes_direct = nodes - return nodes + system_users_ids = [s.id for s in perm.system_users.all()] + _nodes_keys = [n.key for n in perm.nodes.all()] + iterable = itertools.product(_nodes_keys, system_users_ids, actions) + for node_key, sys_id, action in iterable: + nodes_keys[node_key][sys_id] |= action + + self.tree.add_nodes(nodes_keys) + + pattern = set() + for key in nodes_keys: + pattern.add(r'^{0}$|^{0}:'.format(key)) + pattern = '|'.join(list(pattern)) + if pattern: + assets_ids = Asset.objects.filter( + nodes__key__regex=pattern + ).values_list("id", flat=True).distinct() + else: + assets_ids = [] + self.tree.add_assets_without_system_users(assets_ids) + self._nodes_direct = nodes_keys + return nodes_keys def get_nodes_without_cache(self): - self.get_assets_direct() + self.get_assets_without_cache() return self.tree.get_nodes() - #@timeit + @timeit def get_assets_direct(self): """ - 返回用户授权规则直接关联的资产 - :return: {asset1: {system_user1: 1,}} + 返回直接授权的资产, + 并添加到tree.assets中 + :return: + {asset.id: {system_user.id: actions, }, } """ if self._assets_direct: return self._assets_direct - assets = defaultdict(lambda: defaultdict(int)) + assets_ids = defaultdict(lambda: defaultdict(int)) for perm in self.permissions: actions = [perm.actions] - _assets = perm.assets.valid().only(*self.assets_only) - system_users = perm.system_users.all() - iterable = itertools.product(_assets, system_users, actions) - for asset, system_user, action in iterable: - assets[asset][system_user] |= action - self.tree.add_assets(assets) - self._assets_direct = assets - return assets + _assets_ids = [a.id for a in perm.assets.all()] + system_users_ids = [s.id for s in perm.system_users.all()] + iterable = itertools.product(_assets_ids, system_users_ids, actions) + for asset_id, sys_id, action in iterable: + assets_ids[asset_id][sys_id] |= action + self.tree.add_assets(assets_ids) + self._assets_direct = assets_ids + return assets_ids - #@timeit + @timeit def get_assets_without_cache(self): """ - :return: {asset1: set(system_user1,)} + :return: + [ + {"id": asset.id, "system_users": {system_user.id: actions, }}, + ] """ if self._assets: return self._assets + self.get_nodes_direct() self.get_assets_direct() - nodes = self.get_nodes_direct() - pattern = set() - for node in nodes: - pattern.add(r'^{0}$|^{0}:'.format(node.key)) - pattern = '|'.join(list(pattern)) - if pattern: - assets = Asset.objects.filter(nodes__key__regex=pattern).valid() \ - .prefetch_related('nodes')\ - .only(*self.assets_only)\ - .distinct() - else: - assets = [] - assets = list(assets) - self.tree.add_assets_without_system_users(assets) assets = self.tree.get_assets() self._assets = assets return assets - #@timeit + @timeit def get_nodes_with_assets_without_cache(self): - """ - 返回节点并且包含资产 - {"node": {"asset": {"system_user": 1})}} - :return: - """ self.get_assets_without_cache() nodes_assets = self.tree.get_nodes_with_assets() return nodes_assets @@ -545,67 +583,72 @@ def sort_assets(assets, order_by='hostname', reverse=False): return assets -def parse_node_to_tree_node(node): - name = '{} ({})'.format(node.value, node.assets_amount) - data = { - 'id': node.key, - 'name': name, - 'title': name, - 'pId': node.parent_key, - 'isParent': True, - 'open': node.is_root(), - 'meta': { - 'node': { - "id": node.id, - "key": node.key, - "value": node.value, - }, - 'type': 'node' - } - } - tree_node = TreeNode(**data) - return tree_node +class ParserNode: + nodes_only_fields = ("key", "value", "id") + assets_only_fields = ("platform", "hostname", "id", "ip", "protocols") + system_users_only_fields = ( + "id", "name", "username", "protocol", "priority", "login_mode", + ) - -def parse_asset_to_tree_node(node, asset, system_users): - icon_skin = 'file' - if asset.platform.lower() == 'windows': - icon_skin = 'windows' - elif asset.platform.lower() == 'linux': - icon_skin = 'linux' - _system_users = [] - for system_user, action in system_users.items(): - _system_users.append({ - 'id': system_user.id, - 'name': system_user.name, - 'username': system_user.username, - 'protocol': system_user.protocol, - 'priority': system_user.priority, - 'login_mode': system_user.login_mode, - 'actions': [Action.value_to_choices(action)], - }) - data = { - 'id': str(asset.id), - 'name': asset.hostname, - 'title': asset.ip, - 'pId': node.key, - 'isParent': False, - 'open': False, - 'iconSkin': icon_skin, - 'meta': { - 'system_users': _system_users, - 'type': 'asset', - 'asset': { - 'id': asset.id, - 'hostname': asset.hostname, - 'ip': asset.ip, - 'protocols': asset.protocols_as_list, - 'platform': asset.platform, - 'domain': None if not asset.domain else asset.domain.id, - 'is_active': asset.is_active, - 'comment': asset.comment - }, + @staticmethod + def parse_node_to_tree_node(node): + name = '{} ({})'.format(node.value, node.assets_amount) + data = { + 'id': node.key, + 'name': name, + 'title': name, + 'pId': node.parent_key, + 'isParent': True, + 'open': node.is_root(), + 'meta': { + 'node': { + "id": node.id, + "key": node.key, + "value": node.value, + }, + 'type': 'node' + } } - } - tree_node = TreeNode(**data) - return tree_node + tree_node = TreeNode(**data) + return tree_node + + @staticmethod + def parse_asset_to_tree_node(node, asset, system_users): + icon_skin = 'file' + if asset.platform.lower() == 'windows': + icon_skin = 'windows' + elif asset.platform.lower() == 'linux': + icon_skin = 'linux' + _system_users = [] + for system_user in system_users: + _system_users.append({ + 'id': system_user.id, + 'name': system_user.name, + 'username': system_user.username, + 'protocol': system_user.protocol, + 'priority': system_user.priority, + 'login_mode': system_user.login_mode, + 'actions': [Action.value_to_choices(system_user.actions)], + }) + data = { + 'id': str(asset.id), + 'name': asset.hostname, + 'title': asset.ip, + 'pId': node.key, + 'isParent': False, + 'open': False, + 'iconSkin': icon_skin, + 'meta': { + 'system_users': _system_users, + 'type': 'asset', + 'asset': { + 'id': asset.id, + 'hostname': asset.hostname, + 'ip': asset.ip, + 'protocols': asset.protocols_as_list, + 'platform': asset.platform, + }, + } + } + tree_node = TreeNode(**data) + return tree_node diff --git a/apps/perms/utils/stack.py b/apps/perms/utils/stack.py new file mode 100644 index 000000000..ac2e8b69f --- /dev/null +++ b/apps/perms/utils/stack.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# +from collections import defaultdict +from common.struct import Stack +from common.utils import timeit +from assets.utils import NodeUtil + + +class PermStackUtilMixin: + def __init__(self, debug=False): + self.stack = None + self._nodes = {} + self._debug = debug + + @staticmethod + def sorted_by(node_dict): + return [int(i) for i in node_dict['key'].split(':')] + + @staticmethod + def is_children(item1, item2): + key1 = item1["key"] + key2 = item2["key"] + return key2.startswith(key1 + ':') and ( + len(key2.split(':')) - len(key1.split(':')) + ) == 1 + + def debug(self, msg): + self._debug and print(msg) + + +class PermSystemUserNodeUtil(PermStackUtilMixin): + """ + self._nodes: {node.key: {system_user.id: actions,}} + """ + @timeit + def get_nodes_family_and_system_users(self, nodes_with_system_users): + """ + 返回所有nodes_with_system_users中的node的家族节点的信息, + 并子会继承祖先的系统用户和actions信息 + :param nodes_with_system_users: + {node.key: {system_user.id: actions,}, } + :return: + {node.key: {system_user.id: actions,}, } + """ + node_util = NodeUtil() + _nodes_keys = nodes_with_system_users.keys() + family_keys = node_util.get_some_nodes_family_keys_by_keys(_nodes_keys) + + nodes_items = [] + for i in family_keys: + system_users = nodes_with_system_users.get(i, defaultdict(int)) + item = {"key": i, "system_users": system_users} + nodes_items.append(item) + # 按照父子关系排序 + nodes_items.sort(key=self.sorted_by) + nodes_items.append({"key": "", "system_users": defaultdict(int)}) + + self.stack = Stack() + for item in nodes_items: + self.debug("准备: {} 栈顶: {}".format( + item['key'], self.stack.top["key"] if self.stack.top else None) + ) + # 入栈之前检查,该节点是不是栈顶节点的子节点 + # 如果不是,则栈顶出栈 + while self.stack.top and not self.is_children(self.stack.top, item): + # 出栈 + self.pop_from_stack_system_users() + # 入栈 + self.push_to_stack_system_users(item) + # 出栈最后一个 + self.debug("剩余: {}".format(', '.join([n["key"] for n in self.stack]))) + return self._nodes + + def push_to_stack_system_users(self, item): + """ + :param item: + {"key": node.key, "system_users": {system_user.id: actions,},} + """ + if not self.stack.is_empty(): + item_system_users = item["system_users"] + for system_user, action in self.stack.top["system_users"].items(): + # 更新栈顶的系统用户和action到将要入栈的item中 + item_system_users[system_user] |= action + item["system_users"] = item_system_users + self.debug("入栈: {}".format(item['key'])) + self.stack.push(item) + + # 出栈 + def pop_from_stack_system_users(self): + _node = self.stack.pop() + self._nodes[_node["key"]] = _node["system_users"] + self.debug("出栈: {} 栈顶: {}".format(_node['key'], self.stack.top['key'] if self.stack.top else None)) + + +class PermAssetsAmountUtil(PermStackUtilMixin): + def push_to_stack_nodes_amount(self, item): + self.debug("入栈: {}".format(item['key'])) + self.stack.push(item) + + def pop_from_stack_nodes_amount(self): + _node = self.stack.pop() + self.debug("出栈: {} 栈顶: {}".format( + _node['key'], self.stack.top['key'] if self.stack.top else None) + ) + _node["assets_amount"] = len(_node["all_assets"] | _node["assets"]) + self._nodes[_node.pop("key")] = _node + + if not self.stack.top: + return + self.stack.top["all_assets"]\ + .update(_node["all_assets"] | _node["assets"]) + + def compute_nodes_assets_amount(self, nodes_with_assets): + self.stack = Stack() + nodes_items = [] + for key, values in nodes_with_assets.items(): + nodes_items.append({ + "key": key, "assets": values["assets"], + "all_assets": values["all_assets"], "assets_amount": 0 + }) + + nodes_items.sort(key=self.sorted_by) + nodes_items.append({"key": "", "assets": set(), "all_assets": set(), "assets_amount": 0}) + self.stack = Stack() + for item in nodes_items: + self.debug("准备: {} 栈顶: {}".format( + item['key'], self.stack.top["key"] if self.stack.top else None) + ) + # 入栈之前检查,该节点是不是栈顶节点的子节点 + # 如果不是,则栈顶出栈 + while self.stack.top and not self.is_children(self.stack.top, item): + self.pop_from_stack_nodes_amount() + self.push_to_stack_nodes_amount(item) + # 出栈最后一个 + self.debug("剩余: {}".format(', '.join([n["key"] for n in self.stack]))) + return self._nodes \ No newline at end of file diff --git a/apps/perms/views/asset_permission.py b/apps/perms/views/asset_permission.py index 2981c9910..3119b9863 100644 --- a/apps/perms/views/asset_permission.py +++ b/apps/perms/views/asset_permission.py @@ -163,12 +163,12 @@ class AssetPermissionAssetView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - assets_granted = self.get_queryset() + granted_nodes = self.object.nodes.all() + nodes_remain = [n for n in Node.get_queryset() if n not in granted_nodes] context = { 'app': _('Perms'), 'action': _('Asset permission asset list'), - 'assets_remain': Asset.objects.exclude(id__in=[a.id for a in assets_granted]), - 'nodes_remain': Node.objects.exclude(granted_by_permissions=self.object), + 'nodes_remain': nodes_remain, } kwargs.update(context) return super().get_context_data(**kwargs) \ 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) 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/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 352b4a654..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,9 +1109,22 @@ 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); - return date.toISOString().replace("T", " ").replace(/\..*/, ""); + var date_s = date.toLocaleString(navigator.language, {hour12: false}); + return date_s.split("/").join('-') } function getUrlParams(url) { @@ -1137,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/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: 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/models/user.py b/apps/users/models/user.py index dbc336443..88abe4dd3 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -27,97 +27,7 @@ signer = get_signer() logger = get_logger(__file__) -class User(AbstractUser): - ROLE_ADMIN = 'Admin' - ROLE_USER = 'User' - ROLE_APP = 'App' - ROLE_AUDITOR = 'Auditor' - - ROLE_CHOICES = ( - (ROLE_ADMIN, _('Administrator')), - (ROLE_USER, _('User')), - (ROLE_APP, _('Application')), - (ROLE_AUDITOR, _("Auditor")) - ) - OTP_LEVEL_CHOICES = ( - (0, _('Disable')), - (1, _('Enable')), - (2, _("Force enable")), - ) - SOURCE_LOCAL = 'local' - SOURCE_LDAP = 'ldap' - SOURCE_OPENID = 'openid' - SOURCE_RADIUS = 'radius' - SOURCE_CHOICES = ( - (SOURCE_LOCAL, 'Local'), - (SOURCE_LDAP, 'LDAP/AD'), - (SOURCE_OPENID, 'OpenID'), - (SOURCE_RADIUS, 'Radius'), - ) - - CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}" - - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - username = models.CharField( - max_length=128, unique=True, verbose_name=_('Username') - ) - name = models.CharField(max_length=128, verbose_name=_('Name')) - email = models.EmailField( - max_length=128, unique=True, verbose_name=_('Email') - ) - groups = models.ManyToManyField( - 'users.UserGroup', related_name='users', - blank=True, verbose_name=_('User group') - ) - role = models.CharField( - choices=ROLE_CHOICES, default='User', max_length=10, - blank=True, verbose_name=_('Role') - ) - avatar = models.ImageField( - upload_to="avatar", null=True, verbose_name=_('Avatar') - ) - wechat = models.CharField( - max_length=128, blank=True, verbose_name=_('Wechat') - ) - phone = models.CharField( - max_length=20, blank=True, null=True, verbose_name=_('Phone') - ) - otp_level = models.SmallIntegerField( - default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('MFA') - ) - otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True) - # Todo: Auto generate key, let user download - private_key = fields.EncryptTextField( - blank=True, null=True, verbose_name=_('Private key') - ) - public_key = fields.EncryptTextField( - blank=True, null=True, verbose_name=_('Public key') - ) - comment = models.TextField( - blank=True, null=True, verbose_name=_('Comment') - ) - is_first_login = models.BooleanField(default=True) - date_expired = models.DateTimeField( - default=date_expired_default, blank=True, null=True, - db_index=True, verbose_name=_('Date expired') - ) - created_by = models.CharField( - max_length=30, default='', verbose_name=_('Created by') - ) - source = models.CharField( - max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES, - verbose_name=_('Source') - ) - date_password_last_updated = models.DateTimeField( - auto_now_add=True, blank=True, null=True, - verbose_name=_('Date password last updated') - ) - - user_cache_key_prefix = '_User_{}' - - def __str__(self): - return '{0.name}({0.username})'.format(self) - +class AuthMixin: @property def password_raw(self): raise AttributeError('Password raw is not a readable attribute') @@ -134,9 +44,11 @@ class User(AbstractUser): def set_password(self, raw_password): self._set_password = True if self.can_update_password(): - return super().set_password(raw_password) + self.date_password_last_updated = timezone.now() + super().set_password(raw_password) else: - error = _("User auth from {}, go there change password").format(self.source) + error = _("User auth from {}, go there change password").format( + self.source) raise PermissionError(error) def can_update_password(self): @@ -146,9 +58,6 @@ class User(AbstractUser): from ..utils import check_otp_code return check_otp_code(self.otp_secret_key, code) - def get_absolute_url(self): - return reverse('users:user-detail', args=(self.id,)) - def is_public_key_valid(self): """ Check if the user's ssh public key is valid. @@ -158,36 +67,12 @@ class User(AbstractUser): return True return False - @property - def groups_display(self): - return ' '.join([group.name for group in self.groups.all()]) - - @property - def role_display(self): - return self.get_role_display() - - @property - def source_display(self): - return self.get_source_display() - - @property - def is_expired(self): - if self.date_expired and self.date_expired < timezone.now(): - return True - else: - return False - - @property - def is_valid(self): - if self.is_active and not self.is_expired: - return True - return False - @property def public_key_obj(self): class PubKey(object): def __getattr__(self, item): return '' + if self.public_key: import sshpubkeys try: @@ -196,6 +81,53 @@ class User(AbstractUser): pass return PubKey() + def reset_password(self, new_password): + self.set_password(new_password) + self.save() + + @property + def date_password_expired(self): + interval = settings.SECURITY_PASSWORD_EXPIRATION_TIME + date_expired = self.date_password_last_updated + timezone.timedelta( + days=int(interval)) + return date_expired + + @property + def password_expired_remain_days(self): + date_remain = self.date_password_expired - timezone.now() + return date_remain.days + + @property + def password_has_expired(self): + if self.is_local and self.password_expired_remain_days < 0: + return True + return False + + @property + def password_will_expired(self): + if self.is_local and self.password_expired_remain_days < 5: + return True + return False + + +class RoleMixin: + ROLE_ADMIN = 'Admin' + ROLE_USER = 'User' + ROLE_APP = 'App' + ROLE_AUDITOR = 'Auditor' + + ROLE_CHOICES = ( + (ROLE_ADMIN, _('Administrator')), + (ROLE_USER, _('User')), + (ROLE_APP, _('Application')), + (ROLE_AUDITOR, _("Auditor")) + ) + role = ROLE_USER + + @property + def role_display(self): + return self.get_role_display() + @property def is_superuser(self): if self.role == 'Admin': @@ -251,41 +183,21 @@ class User(AbstractUser): def is_staff(self, value): pass - @property - def is_local(self): - return self.source == self.SOURCE_LOCAL - - @property - def date_password_expired(self): - interval = settings.SECURITY_PASSWORD_EXPIRATION_TIME - date_expired = self.date_password_last_updated + timezone.timedelta( - days=int(interval)) - return date_expired + @classmethod + def create_app_user(cls, name, comment): + app = cls.objects.create( + username=name, name=name, email='{}@local.domain'.format(name), + is_active=False, role='App', comment=comment, + is_first_login=False, created_by='System' + ) + access_key = app.create_access_key() + return app, access_key - @property - def password_expired_remain_days(self): - date_remain = self.date_password_expired - timezone.now() - return date_remain.days - @property - def password_has_expired(self): - if self.is_local and self.password_expired_remain_days < 0: - return True - return False - - @property - def password_will_expired(self): - if self.is_local and self.password_expired_remain_days < 5: - return True - return False - - def save(self, *args, **kwargs): - if not self.name: - self.name = self.username - if self.username == 'admin': - self.role = 'Admin' - self.is_active = True - super().save(*args, **kwargs) +class TokenMixin: + CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}" + email = '' + id = None @property def private_token(self): @@ -333,31 +245,12 @@ class User(AbstractUser): def access_key(self): return self.access_keys.first() - def is_member_of(self, user_group): - if user_group in self.groups.all(): - return True - return False - - def avatar_url(self): - admin_default = settings.STATIC_URL + "img/avatar/admin.png" - user_default = settings.STATIC_URL + "img/avatar/user.png" - if self.avatar: - return self.avatar.url - if self.is_superuser: - return admin_default - else: - return user_default - def generate_reset_token(self): letter = string.ascii_letters + string.digits token = ''.join([random.choice(letter) for _ in range(50)]) self.set_cache(token) return token - def set_cache(self, token): - key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) - cache.set(key, {'id': self.id, 'email': self.email}, 3600) - @classmethod def validate_reset_password_token(cls, token): try: @@ -371,11 +264,25 @@ class User(AbstractUser): user = None return user + def set_cache(self, token): + key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) + cache.set(key, {'id': self.id, 'email': self.email}, 3600) + @classmethod def expired_reset_password_token(cls, token): key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) cache.delete(key) + +class MFAMixin: + otp_level = 0 + otp_secret_key = '' + OTP_LEVEL_CHOICES = ( + (0, _('Disable')), + (1, _('Enable')), + (2, _("Force enable")), + ) + @property def otp_enabled(self): return self.otp_force_enabled or self.otp_level > 0 @@ -397,39 +304,130 @@ class User(AbstractUser): self.otp_level = 0 self.otp_secret_key = None - def to_json(self): - return OrderedDict({ - 'id': self.id, - 'username': self.username, - 'name': self.name, - 'email': self.email, - 'is_active': self.is_active, - 'is_superuser': self.is_superuser, - 'role': self.get_role_display(), - 'groups': [group.name for group in self.groups.all()], - 'source': self.get_source_display(), - 'wechat': self.wechat, - 'phone': self.phone, - 'otp_level': self.otp_level, - 'comment': self.comment, - 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') \ - if self.date_expired is not None else None - }) - @classmethod - def create_app_user(cls, name, comment): - app = cls.objects.create( - username=name, name=name, email='{}@local.domain'.format(name), - is_active=False, role='App', comment=comment, - is_first_login=False, created_by='System' - ) - access_key = app.create_access_key() - return app, access_key +class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): + SOURCE_LOCAL = 'local' + SOURCE_LDAP = 'ldap' + SOURCE_OPENID = 'openid' + SOURCE_RADIUS = 'radius' + SOURCE_CHOICES = ( + (SOURCE_LOCAL, 'Local'), + (SOURCE_LDAP, 'LDAP/AD'), + (SOURCE_OPENID, 'OpenID'), + (SOURCE_RADIUS, 'Radius'), + ) - def reset_password(self, new_password): - self.set_password(new_password) - self.date_password_last_updated = timezone.now() - self.save() + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + username = models.CharField( + max_length=128, unique=True, verbose_name=_('Username') + ) + name = models.CharField(max_length=128, verbose_name=_('Name')) + email = models.EmailField( + max_length=128, unique=True, verbose_name=_('Email') + ) + groups = models.ManyToManyField( + 'users.UserGroup', related_name='users', + blank=True, verbose_name=_('User group') + ) + role = models.CharField( + choices=RoleMixin.ROLE_CHOICES, default='User', max_length=10, + blank=True, verbose_name=_('Role') + ) + avatar = models.ImageField( + upload_to="avatar", null=True, verbose_name=_('Avatar') + ) + wechat = models.CharField( + max_length=128, blank=True, verbose_name=_('Wechat') + ) + phone = models.CharField( + max_length=20, blank=True, null=True, verbose_name=_('Phone') + ) + otp_level = models.SmallIntegerField( + default=0, choices=MFAMixin.OTP_LEVEL_CHOICES, verbose_name=_('MFA') + ) + otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True) + # Todo: Auto generate key, let user download + private_key = fields.EncryptTextField( + blank=True, null=True, verbose_name=_('Private key') + ) + public_key = fields.EncryptTextField( + blank=True, null=True, verbose_name=_('Public key') + ) + comment = models.TextField( + blank=True, null=True, verbose_name=_('Comment') + ) + is_first_login = models.BooleanField(default=True) + date_expired = models.DateTimeField( + default=date_expired_default, blank=True, null=True, + db_index=True, verbose_name=_('Date expired') + ) + created_by = models.CharField( + max_length=30, default='', verbose_name=_('Created by') + ) + source = models.CharField( + max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES, + verbose_name=_('Source') + ) + date_password_last_updated = models.DateTimeField( + auto_now_add=True, blank=True, null=True, + verbose_name=_('Date password last updated') + ) + + user_cache_key_prefix = '_User_{}' + + def __str__(self): + return '{0.name}({0.username})'.format(self) + + def get_absolute_url(self): + return reverse('users:user-detail', args=(self.id,)) + + @property + def groups_display(self): + return ' '.join([group.name for group in self.groups.all()]) + + @property + def source_display(self): + return self.get_source_display() + + @property + def is_expired(self): + if self.date_expired and self.date_expired < timezone.now(): + return True + else: + return False + + @property + def is_valid(self): + if self.is_active and not self.is_expired: + return True + return False + + @property + def is_local(self): + return self.source == self.SOURCE_LOCAL + + def save(self, *args, **kwargs): + if not self.name: + self.name = self.username + if self.username == 'admin': + self.role = 'Admin' + self.is_active = True + super().save(*args, **kwargs) + + def is_member_of(self, user_group): + if user_group in self.groups.all(): + return True + return False + + def avatar_url(self): + admin_default = settings.STATIC_URL + "img/avatar/admin.png" + user_default = settings.STATIC_URL + "img/avatar/user.png" + if self.avatar: + return self.avatar.url + if self.is_superuser: + return admin_default + else: + return user_default def delete(self, using=None, keep_parents=False): if self.pk == 1 or self.username == 'admin': 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/_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_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 #}
-
-
-
-
-
-
- -
-
-
-
-
-
-
- - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Active' %}{% trans 'System users' %}
-
-
+ {% include 'users/_granted_assets.html' %}
@@ -60,88 +32,9 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html index c0d99876f..ebe12af84 100644 --- a/apps/users/templates/users/user_group_detail.html +++ b/apps/users/templates/users/user_group_detail.html @@ -142,7 +142,7 @@ function updateGroupMember(users) { // clear jumpserver.selected_groups jumpserver.users_selected = {}; }; - APIUpdateAttr({ + requestApi({ url: the_url, body: JSON.stringify(body), success: success diff --git a/apps/users/templates/users/user_group_granted_asset.html b/apps/users/templates/users/user_group_granted_asset.html index e4a8d77f0..ba0119050 100644 --- a/apps/users/templates/users/user_group_granted_asset.html +++ b/apps/users/templates/users/user_group_granted_asset.html @@ -23,142 +23,21 @@
-
-
-
-
-
-
- -
-
-
-
-
-
-
- - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Active' %}{% trans 'Reachable' %}
-
-
+ {% include 'users/_granted_assets.html' %}
- {% endblock %} + {% 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)