diff --git a/README.md b/README.md index e44a23f9c..f0bceec9b 100644 --- a/README.md +++ b/README.md @@ -19,31 +19,25 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点 ---- ### 功能 - - 统一认证 - - 资产管理 - - 统一授权 - - 审计 - - 支持LDAP认证 - - Web terminal - - SSH Server - - 支持Windows RDP + + ![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver13.jpg "Jumpserver功能") ### 开始使用 -快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/latest/quickstart.html) +快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html) -一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/latest/step_by_step.html) +一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html) 也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org) -### Demo 和 截图 +### Demo 和 截图 我们提供了DEMO和截图可以让你快速了解Jumpserver [DEMO](http://demo.jumpserver.org) [截图](http://docs.jumpserver.org/zh/docs/snapshot.html) -### SDK +### SDK 我们还编写了一些SDK,供你其它系统快速和Jumpserver APi交互, diff --git a/apps/__init__.py b/apps/__init__.py index def994bcd..7e96164aa 100644 --- a/apps/__init__.py +++ b/apps/__init__.py @@ -2,4 +2,4 @@ # -*- coding: utf-8 -*- # -__version__ = "1.2.1" +__version__ = "1.3.2" diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 50c037df9..8c1f3d726 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # +import random + from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet @@ -11,9 +13,8 @@ from django.db.models import Q from common.mixins import IDInFilterMixin from common.utils import get_logger -from ..hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser, \ - NodePermissionUtil -from ..models import Asset, SystemUser, AdminUser, Node +from ..hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser +from ..models import Asset, SystemUser, AdminUser, Node from .. import serializers from ..tasks import update_asset_hardware_info_manual, \ test_asset_connectability_manual @@ -22,8 +23,9 @@ from ..utils import LabelFilter logger = get_logger(__file__) __all__ = [ - 'AssetViewSet', 'UserAssetListView', 'AssetListUpdateApi', - 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi' + 'AssetViewSet', 'AssetListUpdateApi', + 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', + 'AssetGatewayApi' ] @@ -40,32 +42,34 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): permission_classes = (IsSuperUserOrAppUser,) def get_queryset(self): - queryset = super().get_queryset() + queryset = super().get_queryset()\ + .prefetch_related('labels', 'nodes')\ + .select_related('admin_user') admin_user_id = self.request.query_params.get('admin_user_id') node_id = self.request.query_params.get("node_id") + show_current_asset = self.request.query_params.get("show_current_asset") if admin_user_id: admin_user = get_object_or_404(AdminUser, id=admin_user_id) queryset = queryset.filter(admin_user=admin_user) - if node_id: + + if node_id and show_current_asset: node = get_object_or_404(Node, id=node_id) - if not node.is_root(): + if node.is_root(): queryset = queryset.filter( - nodes__key__regex='{}(:[0-9]+)*$'.format(node.key), + Q(nodes=node_id) | Q(nodes__isnull=True) ).distinct() - return queryset + else: + queryset = queryset.filter(nodes=node).distinct() - -class UserAssetListView(generics.ListAPIView): - queryset = Asset.objects.all() - serializer_class = serializers.AssetSerializer - permission_classes = (IsValidUser,) - - def get_queryset(self): - assets_granted = NodePermissionUtil.get_user_assets(self.request.user).keys() - queryset = self.queryset.filter( - id__in=[asset.id for asset in assets_granted] - ) + if node_id and not show_current_asset: + node = get_object_or_404(Node, id=node_id) + if node.is_root(): + queryset = Asset.objects.all() + else: + queryset = queryset.filter( + nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key), + ).distinct() return queryset @@ -105,3 +109,20 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView): asset = get_object_or_404(Asset, pk=asset_id) task = test_asset_connectability_manual.delay(asset) return Response({"task": task.id}) + + +class AssetGatewayApi(generics.RetrieveAPIView): + queryset = Asset.objects.all() + permission_classes = (IsSuperUserOrAppUser,) + + def retrieve(self, request, *args, **kwargs): + asset_id = kwargs.get('pk') + asset = get_object_or_404(Asset, pk=asset_id) + + if asset.domain and \ + asset.domain.gateways.filter(protocol=asset.protocol).exists(): + gateway = random.choice(asset.domain.gateways.filter(protocol=asset.protocol)) + serializer = serializers.GatewayWithAuthSerializer(instance=gateway) + return Response(serializer.data) + else: + return Response({"msg": "Not have gateway"}, status=404) \ No newline at end of file diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index f123e4649..e5ace021e 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -14,6 +14,7 @@ # limitations under the License. from rest_framework import generics, mixins +from rest_framework.serializers import ValidationError from rest_framework.views import APIView from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet @@ -30,7 +31,7 @@ from .. import serializers logger = get_logger(__file__) __all__ = [ 'NodeViewSet', 'NodeChildrenApi', - 'NodeAssetsApi', 'NodeWithAssetsApi', + 'NodeAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', @@ -49,32 +50,32 @@ class NodeViewSet(BulkModelViewSet): serializer.save() -class NodeWithAssetsApi(generics.ListAPIView): - permission_classes = (IsSuperUser,) - serializers = serializers.NodeSerializer - - def get_node(self): - pk = self.kwargs.get('pk') or self.request.query_params.get('node') - if not pk: - node = Node.root() - else: - node = get_object_or_404(Node, pk) - return node - - def get_queryset(self): - queryset = [] - node = self.get_node() - children = node.get_children() - assets = node.get_assets() - queryset.extend(list(children)) - - for asset in assets: - node = Node() - node.id = asset.id - node.parent = node.id - node.value = asset.hostname - queryset.append(node) - return queryset +# class NodeWithAssetsApi(generics.ListAPIView): +# permission_classes = (IsSuperUser,) +# serializers = serializers.NodeSerializer +# +# def get_node(self): +# pk = self.kwargs.get('pk') or self.request.query_params.get('node') +# if not pk: +# node = Node.root() +# else: +# node = get_object_or_404(Node, pk) +# return node +# +# def get_queryset(self): +# queryset = [] +# node = self.get_node() +# children = node.get_children() +# assets = node.get_assets() +# queryset.extend(list(children)) +# +# for asset in assets: +# node = Node() +# node.id = asset.id +# node.parent = node.id +# node.value = asset.hostname +# queryset.append(node) +# return queryset class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): @@ -83,16 +84,29 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): serializer_class = serializers.NodeSerializer instance = None + def counter(self): + values = [ + child.value[child.value.rfind(' '):] + for child in self.get_object().get_children() + if child.value.startswith("新节点 ") + ] + values = [int(value) for value in values if value.strip().isdigit()] + count = max(values)+1 if values else 1 + return count + def post(self, request, *args, **kwargs): if not request.data.get("value"): - request.data["value"] = _("New node {}").format( - Node.root().get_next_child_key().split(":")[-1] - ) + request.data["value"] = _("New node {}").format(self.counter()) return super().post(request, *args, **kwargs) def create(self, request, *args, **kwargs): instance = self.get_object() value = request.data.get("value") + values = [child.value for child in instance.get_children()] + if value in values: + raise ValidationError( + 'The same level node name cannot be the same' + ) node = instance.create_child(value=value) return Response( {"id": node.id, "key": node.key, "value": node.value}, @@ -102,7 +116,7 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): def get_object(self): pk = self.kwargs.get('pk') or self.request.query_params.get('id') if not pk: - node = Node.root() + node = None else: node = get_object_or_404(Node, pk=pk) return node @@ -112,7 +126,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): query_all = self.request.query_params.get("all") query_assets = self.request.query_params.get('assets') node = self.get_object() - if node == Node.root(): + if node is None: + node = Node.root() queryset.append(node) if query_all: children = node.get_all_children() @@ -125,10 +140,11 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): for asset in assets: node_fake = Node() node_fake.id = asset.id - node_fake.parent = node + node_fake.is_node = False + node_fake.parent_id = node.id node_fake.value = asset.hostname - node_fake.is_asset = True queryset.append(node_fake) + queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True) return queryset def get(self, request, *args, **kwargs): @@ -163,7 +179,6 @@ class NodeAddChildrenApi(generics.UpdateAPIView): if not node: continue node.parent = instance - node.save() return Response("OK") @@ -190,6 +205,9 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView): instance = self.get_object() if instance != Node.root(): instance.assets.remove(*tuple(assets)) + else: + assets = [asset for asset in assets if asset.nodes.count() > 1] + instance.assets.remove(*tuple(assets)) class NodeReplaceAssetsApi(generics.UpdateAPIView): diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index b4e46cc78..66d62232d 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -40,7 +40,7 @@ class SystemUserViewSet(BulkModelViewSet): permission_classes = (IsSuperUserOrAppUser,) -class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView): +class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ Get system user auth info """ @@ -48,6 +48,11 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView): permission_classes = (IsSuperUserOrAppUser,) serializer_class = serializers.SystemUserAuthSerializer + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.clear_auth() + return Response(status=204) + class SystemUserPushApi(generics.RetrieveAPIView): """ @@ -58,6 +63,9 @@ class SystemUserPushApi(generics.RetrieveAPIView): def retrieve(self, request, *args, **kwargs): system_user = self.get_object() + nodes = system_user.nodes.all() + for node in nodes: + system_user.assets.add(*tuple(node.get_all_assets())) task = push_system_user_to_assets_manual.delay(system_user) return Response({"task": task.id}) diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index f8f187b4d..5000c087d 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -16,7 +16,7 @@ class AssetCreateForm(forms.ModelForm): fields = [ 'hostname', 'ip', 'public_ip', 'port', 'comment', 'nodes', 'is_active', 'admin_user', 'labels', 'platform', - 'domain', + 'domain', 'protocol', ] widgets = { @@ -56,7 +56,7 @@ class AssetUpdateForm(forms.ModelForm): fields = [ 'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', 'public_ip', 'number', 'comment', 'admin_user', 'labels', - 'domain', + 'domain', 'protocol', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index 2295dc005..b25e19d87 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -93,14 +93,21 @@ class SystemUserForm(PasswordAndKeyAuthForm): # Because we define custom field, so we need rewrite :method: `save` system_user = super().save() password = self.cleaned_data.get('password', '') or None + login_mode = self.cleaned_data.get('login_mode', '') or None + protocol = self.cleaned_data.get('protocol') or None auto_generate_key = self.cleaned_data.get('auto_generate_key', False) private_key, public_key = super().gen_keys() + if login_mode == SystemUser.MANUAL_LOGIN or protocol == SystemUser.TELNET_PROTOCOL: + system_user.auto_push = 0 + system_user.save() + if auto_generate_key: logger.info('Auto generate key and set system user auth') system_user.auto_gen_auth() else: system_user.set_auth(password=password, private_key=private_key, public_key=public_key) + return system_user def clean(self): @@ -109,12 +116,24 @@ class SystemUserForm(PasswordAndKeyAuthForm): if not self.instance and not auto_generate: super().validate_password_key() + def is_valid(self): + validated = super().is_valid() + username = self.cleaned_data.get('username') + login_mode = self.cleaned_data.get('login_mode') + if login_mode == SystemUser.AUTO_LOGIN and not username: + self.add_error( + "username", _('* Automatic login mode,' + ' must fill in the username.') + ) + return False + return validated + class Meta: model = SystemUser fields = [ 'name', 'username', 'protocol', 'auto_generate_key', 'password', 'private_key_file', 'auto_push', 'sudo', - 'comment', 'shell', 'priority', + 'comment', 'shell', 'priority', 'login_mode', ] widgets = { 'name': forms.TextInput(attrs={'placeholder': _('Name')}), @@ -124,5 +143,8 @@ class SystemUserForm(PasswordAndKeyAuthForm): 'name': '* required', 'username': '* required', 'auto_push': _('Auto push system user to asset'), - 'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'), - } \ No newline at end of file + 'priority': _('High level will be using login asset as default, ' + 'if user was granted more than 2 system user'), + 'login_mode': _('If you choose manual login mode, you do not ' + 'need to fill in the username and password.') + } diff --git a/apps/assets/hands.py b/apps/assets/hands.py index ad44052d3..a1a376135 100644 --- a/apps/assets/hands.py +++ b/apps/assets/hands.py @@ -14,4 +14,3 @@ from common.mixins import AdminUserRequiredMixin from common.permissions import IsAppUser, IsSuperUser, IsValidUser, IsSuperUserOrAppUser from users.models import User, UserGroup -from perms.utils import NodePermissionUtil diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 5b3009305..7a2b3fe57 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -5,6 +5,7 @@ import uuid import logging import random +from functools import reduce from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -35,6 +36,19 @@ def default_node(): return None +class AssetQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + def valid(self): + return self.active() + + +class AssetManager(models.Manager): + def get_queryset(self): + return AssetQuerySet(self.model, using=self._db) + + class Asset(models.Model): # Important PLATFORM_CHOICES = ( @@ -43,45 +57,89 @@ class Asset(models.Model): ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), + ('Windows2016', 'Windows(2016)'), ('Other', 'Other'), ) + + SSH_PROTOCOL = 'ssh' + RDP_PROTOCOL = 'rdp' + TELNET_PROTOCOL = 'telnet' + PROTOCOL_CHOICES = ( + (SSH_PROTOCOL, 'ssh'), + (RDP_PROTOCOL, 'rdp'), + (TELNET_PROTOCOL, 'telnet (beta)'), + ) + id = models.UUIDField(default=uuid.uuid4, primary_key=True) - ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) - hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) + ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), + db_index=True) + hostname = models.CharField(max_length=128, unique=True, + verbose_name=_('Hostname')) + protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, + choices=PROTOCOL_CHOICES, + verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) - platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) - domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) - nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) + platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, + default='Linux', verbose_name=_('Platform')) + domain = models.ForeignKey("assets.Domain", null=True, blank=True, + related_name='assets', verbose_name=_("Domain"), + on_delete=models.SET_NULL) + nodes = models.ManyToManyField('assets.Node', default=default_node, + related_name='assets', + verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) # Auth - admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user")) + admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, + null=True, verbose_name=_("Admin user")) # Some information - public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) - number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) + public_ip = models.GenericIPAddressField(max_length=32, blank=True, + null=True, + verbose_name=_('Public IP')) + number = models.CharField(max_length=32, null=True, blank=True, + verbose_name=_('Asset number')) # Collect - vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor')) - model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model')) - sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number')) + vendor = models.CharField(max_length=64, null=True, blank=True, + verbose_name=_('Vendor')) + model = models.CharField(max_length=54, null=True, blank=True, + verbose_name=_('Model')) + sn = models.CharField(max_length=128, null=True, blank=True, + verbose_name=_('Serial number')) - cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model')) + cpu_model = models.CharField(max_length=64, null=True, blank=True, + verbose_name=_('CPU model')) cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count')) cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores')) - memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory')) - disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total')) - disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info')) + memory = models.CharField(max_length=64, null=True, blank=True, + verbose_name=_('Memory')) + disk_total = models.CharField(max_length=1024, null=True, blank=True, + verbose_name=_('Disk total')) + disk_info = models.CharField(max_length=1024, null=True, blank=True, + verbose_name=_('Disk info')) - os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS')) - os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version')) - os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch')) - hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw')) + os = models.CharField(max_length=128, null=True, blank=True, + verbose_name=_('OS')) + os_version = models.CharField(max_length=16, null=True, blank=True, + verbose_name=_('OS version')) + os_arch = models.CharField(max_length=16, blank=True, null=True, + verbose_name=_('OS arch')) + hostname_raw = models.CharField(max_length=128, blank=True, null=True, + verbose_name=_('Hostname raw')) - labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) - created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) - date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) - comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) + labels = models.ManyToManyField('assets.Label', blank=True, + related_name='assets', + verbose_name=_("Labels")) + created_by = models.CharField(max_length=32, null=True, blank=True, + verbose_name=_('Created by')) + date_created = models.DateTimeField(auto_now_add=True, null=True, + blank=True, + verbose_name=_('Date created')) + comment = models.TextField(max_length=128, default='', blank=True, + verbose_name=_('Comment')) + + objects = AssetManager() def __str__(self): return '{0.hostname}({0.ip})'.format(self) @@ -103,7 +161,17 @@ class Asset(models.Model): def get_nodes(self): from .node import Node - return self.nodes.all() or [Node.root()] + 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 hardware_info(self): @@ -176,7 +244,8 @@ class Asset(models.Model): seed() for i in range(count): - asset = cls(ip='%s.%s.%s.%s' % (i, i, i, i), + ip = [str(i) for i in random.sample(range(255), 4)] + asset = cls(ip='.'.join(ip), hostname=forgery_py.internet.user_name(True), admin_user=choice(AdminUser.objects.all()), port=22, diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 2997b1b61..908e6b647 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen +from common.validators import alphanumeric from .utils import private_key_validator signer = get_signer() @@ -18,7 +19,7 @@ signer = get_signer() class AssetUser(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) - username = models.CharField(max_length=128, verbose_name=_('Username')) + username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric]) _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key')) @@ -103,10 +104,16 @@ class AssetUser(models.Model): if update_fields: self.save(update_fields=update_fields) + def clear_auth(self): + self._password = '' + self._private_key = '' + self._public_key = '' + self.save() + def auto_gen_auth(self): password = str(uuid.uuid4()) private_key, public_key = ssh_key_gen( - username=self.username, password=password + username=self.username ) self.set_auth(password=password, private_key=private_key, diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index ad806342b..4f4f9ad8b 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -2,9 +2,10 @@ # import uuid -from django.db import models +from django.db import models, transaction +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ - +from common.utils import with_cache __all__ = ['Node'] @@ -12,25 +13,40 @@ __all__ = ['Node'] class Node(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' - value = models.CharField(max_length=128, unique=True, verbose_name=_("Value")) + value = models.CharField(max_length=128, verbose_name=_("Value")) child_mark = models.IntegerField(default=0) date_create = models.DateTimeField(auto_now_add=True) - is_asset = False + is_node = True def __str__(self): return self.full_value + def __eq__(self, other): + return self.key == other.key + + def __gt__(self, other): + if self.is_root(): + return True + self_key = [int(k) for k in self.key.split(':')] + other_key = [int(k) for k in other.key.split(':')] + if len(self_key) < len(other_key): + return True + elif len(self_key) > len(other_key): + return False + else: + return self_key[-1] < other_key[-1] + @property def name(self): return self.value @property def full_value(self): - if self == self.__class__.root(): + ancestor = [a.value for a in self.get_ancestor(with_self=True)] + if self.is_root(): return self.value - else: - return '{} / {}'.format(self.parent.full_value, self.value) + return ' / '.join(ancestor) @property def level(self): @@ -43,77 +59,106 @@ class Node(models.Model): return "{}:{}".format(self.key, mark) def create_child(self, value): - child_key = self.get_next_child_key() - child = self.__class__.objects.create(key=child_key, value=value) - return child + with transaction.atomic(): + child_key = self.get_next_child_key() + child = self.__class__.objects.create(key=child_key, value=value) + return child - def get_children(self): - return self.__class__.objects.filter(key__regex=r'{}:[0-9]+$'.format(self.key)) + def get_children(self, with_self=False): + pattern = r'^{0}$|^{}:[0-9]+$' if with_self else r'^{}:[0-9]+$' + return self.__class__.objects.filter( + key__regex=pattern.format(self.key) + ) - def get_all_children(self): - return self.__class__.objects.filter(key__startswith='{}:'.format(self.key)) + def get_all_children(self, with_self=False): + pattern = r'^{0}$|^{0}:' if with_self else r'^{0}' + return self.__class__.objects.filter( + key__regex=pattern.format(self.key) + ) + + def get_sibling(self, with_self=False): + key = ':'.join(self.key.split(':')[:-1]) + pattern = r'^{}:[0-9]+$'.format(key) + sibling = self.__class__.objects.filter( + key__regex=pattern.format(self.key) + ) + if not with_self: + sibling = sibling.exclude(key=self.key) + return sibling def get_family(self): - children = list(self.get_all_children()) - children.append(self) - return children + ancestor = self.get_ancestor() + children = self.get_all_children() + return [*tuple(ancestor), self, *tuple(children)] def get_assets(self): from .asset import Asset - assets = Asset.objects.filter(nodes__id=self.id) + if self.is_root(): + assets = Asset.objects.filter( + Q(nodes__id=self.id) | Q(nodes__isnull=True) + ) + else: + assets = self.assets.all() return assets - def get_active_assets(self): - return self.get_assets().filter(is_active=True) + def get_valid_assets(self): + return self.get_assets().valid() def get_all_assets(self): from .asset import Asset if self.is_root(): assets = Asset.objects.all() else: - nodes = self.get_family() - assets = Asset.objects.filter(nodes__in=nodes).distinct() + pattern = r'^{0}$|^{0}:'.format(self.key) + assets = Asset.objects.filter(nodes__key__regex=pattern) return assets - def has_assets(self): - return self.get_all_assets() - - def get_all_active_assets(self): - return self.get_all_assets().filter(is_active=True) + def get_all_valid_assets(self): + return self.get_all_assets().valid() def is_root(self): return self.key == '0' @property def parent(self): - if self.key == "0": + if self.key == "0" or not self.key.startswith("0"): return self.__class__.root() - elif not self.key.startswith("0"): - return self.__class__.root() - parent_key = ":".join(self.key.split(":")[:-1]) try: parent = self.__class__.objects.get(key=parent_key) + return parent except Node.DoesNotExist: return self.__class__.root() - else: - return parent @parent.setter def parent(self, parent): - self.key = parent.get_next_child_key() - - @property - def ancestor(self): - if self.parent == self.__class__.root(): - return [self.__class__.root()] + if self.is_node: + children = self.get_all_children() + old_key = self.key + with transaction.atomic(): + self.key = parent.get_next_child_key() + for child in children: + child.key = child.key.replace(old_key, self.key, 1) + child.save() + self.save() else: - return [self.parent, *tuple(self.parent.ancestor)] + self.key = parent.key+':fake' - @property - def ancestor_with_node(self): - ancestor = self.ancestor - ancestor.insert(0, self) + def get_ancestor(self, with_self=False): + if self.is_root(): + ancestor = self.__class__.objects.filter(key='0') + return ancestor + + _key = self.key.split(':') + if not with_self: + _key.pop() + ancestor_keys = [] + for i in range(len(_key)): + ancestor_keys.append(':'.join(_key)) + _key.pop() + ancestor = self.__class__.objects.filter( + key__in=ancestor_keys + ).order_by('key') return ancestor @classmethod @@ -121,4 +166,6 @@ class Node(models.Model): obj, created = cls.objects.get_or_create( key='0', defaults={"key": '0', 'value': "ROOT"} ) + print(obj) return obj + diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index bf31b8491..5faca5da8 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -95,9 +95,18 @@ class AdminUser(AssetUser): class SystemUser(AssetUser): SSH_PROTOCOL = 'ssh' RDP_PROTOCOL = 'rdp' + TELNET_PROTOCOL = 'telnet' PROTOCOL_CHOICES = ( (SSH_PROTOCOL, 'ssh'), (RDP_PROTOCOL, 'rdp'), + (TELNET_PROTOCOL, 'telnet (beta)'), + ) + + AUTO_LOGIN = 'auto' + MANUAL_LOGIN = 'manual' + LOGIN_MODE_CHOICES = ( + (AUTO_LOGIN, _('Automatic login')), + (MANUAL_LOGIN, _('Manually login')) ) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) @@ -107,6 +116,7 @@ class SystemUser(AssetUser): auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) + login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode')) def __str__(self): return '{0.name}({0.username})'.format(self) diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 8556be3bf..e63735794 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -12,36 +12,10 @@ __all__ = [ ] -class NodeTMPSerializer(serializers.ModelSerializer): - parent = serializers.SerializerMethodField() - assets_amount = serializers.SerializerMethodField() - - class Meta: - model = Node - fields = ['id', 'key', 'value', 'parent', 'assets_amount', - 'is_asset'] - list_serializer_class = BulkListSerializer - - @staticmethod - def get_parent(obj): - return obj.parent.id - - @staticmethod - def get_assets_amount(obj): - return obj.get_all_assets().count() - - def get_fields(self): - fields = super().get_fields() - field = fields["key"] - field.required = False - return fields - - class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): """ 资产的数据结构 """ - class Meta: model = Asset list_serializer_class = BulkListSerializer @@ -62,14 +36,14 @@ class AssetGrantedSerializer(serializers.ModelSerializer): """ system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) system_users_join = serializers.SerializerMethodField() - nodes = NodeTMPSerializer(many=True, read_only=True) + # nodes = NodeTMPSerializer(many=True, read_only=True) class Meta: model = Asset fields = ( "id", "hostname", "ip", "port", "system_users_granted", - "is_active", "system_users_join", "os", 'domain', "nodes", - "platform", "comment" + "is_active", "system_users_join", "os", 'domain', + "platform", "comment", "protocol", ) @staticmethod diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index 1c9c385e9..56e01f742 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -48,16 +48,27 @@ class NodeSerializer(serializers.ModelSerializer): class Meta: model = Node - fields = ['id', 'key', 'value', 'parent', 'assets_amount', 'is_asset'] + fields = ['id', 'key', 'value', 'parent', 'assets_amount', 'is_node'] list_serializer_class = BulkListSerializer + def validate(self, data): + value = data.get('value') + instance = self.instance if self.instance else Node.root() + children = instance.parent.get_children().exclude(key=instance.key) + values = [child.value for child in children] + if value in values: + raise serializers.ValidationError( + 'The same level node name cannot be the same' + ) + return data + @staticmethod def get_parent(obj): - return obj.parent.id + return obj.parent.id if obj.is_node else obj.parent_id @staticmethod def get_assets_amount(obj): - return obj.get_all_assets().count() + return obj.get_all_assets().count() if obj.is_node else 0 def get_fields(self): fields = super().get_fields() diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 7abd09d29..7a4e3aadc 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -18,6 +18,13 @@ class SystemUserSerializer(serializers.ModelSerializer): model = SystemUser exclude = ('_password', '_private_key', '_public_key') + def get_field_names(self, declared_fields, info): + fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) + fields.extend([ + 'get_login_mode_display', + ]) + return fields + @staticmethod def get_unreachable_assets(obj): return obj.unreachable_assets @@ -56,7 +63,10 @@ class AssetSystemUserSerializer(serializers.ModelSerializer): """ class Meta: model = SystemUser - fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',) + fields = ( + 'id', 'name', 'username', 'priority', + 'protocol', 'comment', 'login_mode' + ) class SystemUserSimpleSerializer(serializers.ModelSerializer): diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 06cd9f63e..157c88012 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -63,22 +63,26 @@ 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): - if isinstance(instance, Asset) and kwargs['action'] == 'post_add': - logger.debug("Asset node change signal received") - nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users_assets = defaultdict(set) - system_users = SystemUser.objects.filter(nodes__in=nodes) - for system_user in system_users: - system_users_assets[system_user].update({instance}) - for system_user, assets in system_users_assets.items(): - system_user.assets.add(*tuple(assets)) + if isinstance(instance, Asset): + if kwargs['action'] == 'post_add': + logger.debug("Asset node change signal received") + nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + system_users_assets = defaultdict(set) + system_users = SystemUser.objects.filter(nodes__in=nodes) + # 清理节点缓存 + for system_user in system_users: + system_users_assets[system_user].update({instance}) + for system_user, assets in system_users_assets.items(): + system_user.assets.add(*tuple(assets)) @receiver(m2m_changed, sender=Asset.nodes.through) def on_node_assets_changed(sender, instance=None, **kwargs): - if isinstance(instance, Node) and kwargs['action'] == 'post_add': - logger.debug("Node assets change signal received") + if isinstance(instance, Node): assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users = SystemUser.objects.filter(nodes=instance) - for system_user in system_users: - system_user.assets.add(*tuple(assets)) + if kwargs['action'] == 'post_add': + logger.debug("Node assets change signal received") + # 重新关联系统用户和资产的关系 + system_users = SystemUser.objects.filter(nodes=instance) + for system_user in system_users: + system_user.assets.add(*tuple(assets)) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index dd660bc42..f1773df11 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -22,7 +22,7 @@ TIMEOUT = 60 logger = get_logger(__file__) CACHE_MAX_TIME = 60*60*60 disk_pattern = re.compile(r'^hd|sd|xvd|vd') -PERIOD_TASK = os.environ.get("PERIOD_TASK", "on") +PERIOD_TASK = os.environ.get("PERIOD_TASK", "off") @shared_task diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index c5cd48857..faf569137 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -59,7 +59,7 @@ var zTree2, asset_table2 = 0; function initTable2() { var options = { ele: $('#asset_list_modal_table'), - ajax_url: '{% url "api-assets:asset-list" %}', + ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1', columns: [ {data: "id"}, {data: "hostname" }, {data: "ip" } ], @@ -98,7 +98,10 @@ function initTree2() { $.get("{% url 'api-assets:node-list' %}", function(data, status){ $.each(data, function (index, value) { value["pId"] = value["parent"]; - value["open"] = true; + {#value["open"] = true;#} + if (value["key"] === "0") { + value["open"] = true; + } value["name"] = value["value"] + ' (' + value['assets_amount'] + ')'; value['value'] = value['value']; }); diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index 528e271e6..4e1bc51a8 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -36,12 +36,13 @@ {% endif %}

{% trans 'Basic' %}

{% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.login_mode layout="horizontal" %} {% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.priority layout="horizontal" %} {% bootstrap_field form.protocol layout="horizontal" %} +

{% trans 'Auth' %}

{% block auth %} -

{% trans 'Auth' %}

@@ -55,7 +56,7 @@ {% bootstrap_field form.private_key_file layout="horizontal" %}
- +
{{ form.auto_push}}
@@ -79,43 +80,86 @@
{% endblock %} {% block custom_foot_js %} - + } + else if($(login_mode_id).val() === 'auto'){ + $('#auth_title_id').removeClass('hidden'); + $(password_id).closest('.form-group').removeClass('hidden') + protocolChange(); + } +} + +$(document).ready(function () { + $('.select2').select2(); + authFieldsDisplay(); + protocolChange(); + loginModeChange(); +}) +.on('change', protocol_id, function(){ + protocolChange(); +}) +.on('change', auto_generate_key, function(){ + authFieldsDisplay(); +}) +.on('change', login_mode_id, function(){ + loginModeChange(); +}) + + {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index 80ff0cd5f..31314392f 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -124,7 +124,7 @@ $(document).ready(function () { var success = function (data) { 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') + window.open(url, '', 'width=800,height=600,left=400,top=400') }; APIUpdateAttr({ url: the_url, diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index bcb40fb29..15a870800 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -5,7 +5,7 @@ {% block help_message %}
- 管理用户是服务器的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。 + 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。 Windows或其它硬件可以随意设置一个
{% endblock %} @@ -107,6 +107,3 @@ $(document).ready(function(){ }); {% endblock %} - - - diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index 99703d2e3..55e233d0d 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -17,6 +17,7 @@ {% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} + {% bootstrap_field form.protocol layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.domain layout="horizontal" %} @@ -85,14 +86,14 @@ $(document).ready(function () { allowClear: true, templateSelection: format }); - $("#id_platform").change(function (){ - var platform = $("#id_platform option:selected").text(); + $("#id_protocol").change(function (){ + var protocol = $("#id_protocol option:selected").text(); var port = 22; - if(platform === 'Windows'){ + if(protocol === 'rdp'){ port = 3389; } - if(platform === 'Other'){ - port = null; + if(protocol === 'telnet (beta)'){ + port = 23; } $("#id_port").val(port); }); diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index 0b1b2865a..b07a7c348 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -190,7 +190,7 @@ @@ -204,7 +204,7 @@ {% for node in asset.nodes.all %} - {{ node.name }} + {{ node }} diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 04ba11e57..b5e53aaba 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -17,20 +17,21 @@ position:absolute; visibility:hidden; text-align: left; - top: 100%; + {#top: 100%;#} + top: 0; left: 0; z-index: 1000; - float: left; - padding: 5px 0; + {#float: left;#} + padding: 0 0; margin: 2px 0 0; list-style: none; background-clip: padding-box; - } + } div#rMenu li{ margin: 1px 0; cursor: pointer; - {#list-style: none outside none;#} - } + list-style: none outside none; + } .dropdown a:hover { background-color: #f1f1f1 } @@ -47,7 +48,6 @@
-
@@ -87,7 +87,7 @@ {% trans 'IP' %} {% trans 'Hardware' %} {% trans 'Active' %} - {% trans 'Reachable' %} +{# {% trans 'Reachable' %}#} {% trans 'Action' %} @@ -127,6 +127,9 @@
  • +
  • + + @@ -157,26 +160,35 @@ function initTable() { $(td).html('') } }}, - {targets: 5, createdCell: function (td, cellData) { - if (cellData === 'Unknown'){ - $(td).html('') - } else if (!cellData) { - $(td).html('') - } else { - $(td).html('') - } - }}, - {targets: 6, createdCell: function (td, cellData, rowData) { + + {#{targets: 5, createdCell: function (td, cellData) {#} + {# if (cellData === 'Unknown'){#} + {# $(td).html('')#} + {# } else if (!cellData) {#} + {# $(td).html('')#} + {# } else {#} + {# $(td).html('')#} + {# }#} + {# }},#} + + {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) }} ], ajax_url: '{% url "api-assets:asset-list" %}', + + {#columns: [#} + {# {data: "id"}, {data: "hostname" }, {data: "ip" },#} + {# {data: "cpu_cores"}, {data: "is_active", orderable: false },#} + {# {data: "is_connective", orderable: false}, {data: "id", orderable: false }#} + {#],#} + columns: [ {data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "cpu_cores"}, {data: "is_active", orderable: false }, - {data: "is_connective", orderable: false}, {data: "id", orderable: false } + {data: "id", orderable: false } ], op_html: $('#actions').html() }; @@ -200,6 +212,8 @@ function addTreeNode() { }; newNode.checked = zTree.getSelectedNodes()[0].checked; zTree.addNodes(parentNode, 0, newNode); + var node = zTree.getNodeByParam('id', newNode.id, parentNode) + zTree.editName(node); } else { alert("{% trans 'Create node failed' %}") } @@ -230,9 +244,9 @@ function removeTreeNode() { function editTreeNode() { hideRMenu(); - var current_node = zTree.getSelectedNodes()[0]; - if (!current_node){ - return + var current_node = zTree.getSelectedNodes()[0]; + if (!current_node){ + return } if (current_node.value) { current_node.name = current_node.value; @@ -253,6 +267,8 @@ function OnRightClick(event, treeId, treeNode) { function showRMenu(type, x, y) { $("#rMenu ul").show(); x -= 220; + x += document.body.scrollLeft; + y += document.body.scrollTop+document.documentElement.scrollTop; rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"}); $("body").bind("mousedown", onBodyMouseDown); @@ -290,6 +306,7 @@ function onRename(event, treeId, treeNode, isCancel){ function onSelected(event, treeNode) { var url = asset_table.ajax.url(); url = setUrlParam(url, "node_id", treeNode.id); + url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset')); setCookie('node_selected', treeNode.id); asset_table.ajax.url(url); asset_table.ajax.reload(); @@ -385,9 +402,9 @@ function initTree() { $.get("{% url 'api-assets:node-list' %}", function(data, status){ $.each(data, function (index, value) { value["pId"] = value["parent"]; - {#if (value["key"] === "0") {#} - value["open"] = true; - {# }#} + if (value["key"] === "0") { + value["open"] = true; + } value["name"] = value["value"] + ' (' + value['assets_amount'] + ')'; value['value'] = value['value']; }); @@ -417,6 +434,13 @@ function toggle() { $(document).ready(function(){ initTable(); initTree(); + + if(getCookie('show_current_asset') === '1'){ + $('#show_all_asset').css('display', 'inline-block'); + } + else{ + $('#show_current_asset').css('display', 'inline-block'); + } }) .on('click', '.labels li', function () { var val = $(this).text(); @@ -535,6 +559,20 @@ $(document).ready(function(){ flash_message: false }); }) +.on('click', '.btn-show-current-asset', function(){ + hideRMenu(); + $(this).css('display', 'none'); + $('#show_all_asset').css('display', 'inline-block'); + setCookie('show_current_asset', '1'); + location.reload(); +}) +.on('click', '.btn-show-all-asset', function(){ + hideRMenu(); + $(this).css('display', 'none'); + $('#show_current_asset').css('display', 'inline-block'); + setCookie('show_current_asset', ''); + location.reload(); +}) .on('click', '.btn_asset_delete', function () { var $this = $(this); var $data_table = $("#asset_list_table").DataTable(); diff --git a/apps/assets/templates/assets/asset_update.html b/apps/assets/templates/assets/asset_update.html index 3d42ca2b5..7ed1da05a 100644 --- a/apps/assets/templates/assets/asset_update.html +++ b/apps/assets/templates/assets/asset_update.html @@ -21,6 +21,7 @@

    {% trans 'Basic' %}

    {% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} + {% bootstrap_field form.protocol layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} diff --git a/apps/assets/templates/assets/domain_gateway_list.html b/apps/assets/templates/assets/domain_gateway_list.html index 581f6c08a..c2d5528ee 100644 --- a/apps/assets/templates/assets/domain_gateway_list.html +++ b/apps/assets/templates/assets/domain_gateway_list.html @@ -85,6 +85,9 @@ function initTable() { var update_btn = '{% trans "Update" %}'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); var test_btn = '{% trans "Test connection" %}'.replace('{{ DEFAULT_PK }}', cellData); + if(rowData.protocol === 'rdp'){ + test_btn = '{% trans "Test connection" %}'.replace('{{ DEFAULT_PK }}', cellData); + } $(td).html(update_btn + test_btn + del_btn) }} ], @@ -120,7 +123,6 @@ $(document).ready(function(){ success_message: "可连接", fail_message: "连接失败" }) - -}) +}); {% endblock %} diff --git a/apps/assets/templates/assets/domain_list.html b/apps/assets/templates/assets/domain_list.html index 926c4bbc3..a4042d57e 100644 --- a/apps/assets/templates/assets/domain_list.html +++ b/apps/assets/templates/assets/domain_list.html @@ -1,6 +1,14 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %}{% endblock %} + +{% block help_message %} +
    + 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登 +录。 +
    +{% endblock %} + {% block table_container %}
    {% trans "Create domain" %} @@ -69,6 +77,3 @@ $(document).ready(function(){ }); {% endblock %} - - - diff --git a/apps/assets/templates/assets/gateway_create_update.html b/apps/assets/templates/assets/gateway_create_update.html index 7d6800c41..7c1bf14b2 100644 --- a/apps/assets/templates/assets/gateway_create_update.html +++ b/apps/assets/templates/assets/gateway_create_update.html @@ -66,3 +66,28 @@
    {% endblock %} + +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index a02bf1e44..f0ed6030c 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -63,15 +63,19 @@ {{ system_user.username }} - {% trans 'Protocol' %}: - {{ system_user.protocol }} + {% trans 'Login mode' %}: + {{ system_user.get_login_mode_display }} + {% trans 'Protocol' %}: + {{ system_user.protocol }} + + {% trans 'Sudo' %}: {{ system_user.sudo }} {% if system_user.shell %} - + {% trans 'Shell' %}: {{ system_user.shell }} @@ -107,14 +111,14 @@
    -
    +
    {% trans 'Quick update' %}
    - + - {% if system_user.auto_push %} + - {% endif %} + + + + + + {# #} {# #} {# + @@ -48,7 +49,7 @@ function initTable() { var detail_btn = '' + cellData + ''; $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); }}, - {targets: 5, createdCell: function (td, cellData) { + {targets: 6, createdCell: function (td, cellData) { var innerHtml = ""; if (cellData !== 0) { innerHtml = "" + cellData + ""; @@ -57,7 +58,7 @@ function initTable() { } $(td).html('' + innerHtml + ''); }}, - {targets: 6, createdCell: function (td, cellData) { + {targets: 7, createdCell: function (td, cellData) { var innerHtml = ""; if (cellData !== 0) { innerHtml = "" + cellData + ""; @@ -66,7 +67,7 @@ function initTable() { } $(td).html('' + innerHtml + ''); }}, - {targets: 7, createdCell: function (td, cellData, rowData) { + {targets: 8, createdCell: function (td, cellData, rowData) { var val = 0; var innerHtml = ""; var total = rowData.assets_amount; @@ -84,14 +85,14 @@ function initTable() { $(td).html('' + innerHtml + ''); }}, - {targets: 9, createdCell: function (td, cellData, rowData) { + {targets: 10, 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) }}], ajax_url: '{% url "api-assets:system-user-list" %}', columns: [ - {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "assets_amount" }, + {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" }, {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" } ], op_html: $('#actions').html() diff --git a/apps/assets/templates/assets/system_user_update.html b/apps/assets/templates/assets/system_user_update.html index 46ef8d6a3..977c3ac31 100644 --- a/apps/assets/templates/assets/system_user_update.html +++ b/apps/assets/templates/assets/system_user_update.html @@ -4,7 +4,6 @@ {% load bootstrap3 %} {% block auth %} -

    {% trans 'Auth' %}

    {% bootstrap_field form.password layout="horizontal" %} {% bootstrap_field form.private_key_file layout="horizontal" %}
    @@ -15,10 +14,3 @@
    {% endblock %} -{% block custom_foot_js %} - -{% endblock %} \ No newline at end of file diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 4429d0f24..cf55d08ba 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -23,8 +23,8 @@ urlpatterns = [ api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'), url(r'^v1/assets/(?P[0-9a-zA-Z\-]{36})/alive/$', api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), - url(r'^v1/assets/user-assets/$', - api.UserAssetListView.as_view(), name='user-asset-list'), + url(r'^v1/assets/(?P[0-9a-zA-Z\-]{36})/gateway/$', + api.AssetGatewayApi.as_view(), name='asset-gateway'), url(r'^v1/admin-user/(?P[0-9a-zA-Z\-]{36})/nodes/$', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), url(r'^v1/admin-user/(?P[0-9a-zA-Z\-]{36})/auth/$', @@ -35,17 +35,26 @@ urlpatterns = [ api.SystemUserPushApi.as_view(), name='system-user-push'), url(r'^v1/system-user/(?P[0-9a-zA-Z\-]{36})/connective/$', api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/children/$', + api.NodeChildrenApi.as_view(), name='node-children'), url(r'^v1/nodes/children/$', api.NodeChildrenApi.as_view(), name='node-children-2'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/$', api.NodeAssetsApi.as_view(), name='node-assets'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/replace/$', api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/refresh-hardware-info/$', api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'), - url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/test-connective/$', api.TestNodeConnectiveApi.as_view(), name='node-test-connective'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/children/add/$', + api.NodeAddChildrenApi.as_view(), name='node-add-children'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/$', + api.NodeAssetsApi.as_view(), name='node-assets'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/add/$', + api.NodeAddAssetsApi.as_view(), name='node-add-assets'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/replace/$', + api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/remove/$', + api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/refresh-hardware-info/$', + api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/test-connective/$', + api.TestNodeConnectiveApi.as_view(), name='node-test-connective'), - url(r'^v1/gateway/(?P[0-9a-zA-Z\-]{36})/test-connective/$', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), + url(r'^v1/gateway/(?P[0-9a-zA-Z\-]{36})/test-connective/$', + api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), ] urlpatterns += router.urls diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 5fb5eae84..f841e4a79 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -1,7 +1,8 @@ # ~*~ coding: utf-8 ~*~ # - +import os import paramiko +from paramiko.ssh_exception import SSHException from common.utils import get_object_or_none from .models import Asset, SystemUser, Label @@ -49,22 +50,23 @@ def test_gateway_connectability(gateway): """ client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - proxy_command = [ - "ssh", "{}@{}".format(gateway.username, gateway.ip), - "-p", str(gateway.port), "-W", "127.0.0.1:{}".format(gateway.port), - ] - - if gateway.password: - proxy_command.insert(0, "sshpass -p '{}'".format(gateway.password)) - if gateway.private_key: - proxy_command.append("-i {}".format(gateway.private_key_file)) + proxy = paramiko.SSHClient() + proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - sock = paramiko.ProxyCommand(" ".join(proxy_command)) - except paramiko.ProxyCommandFailure as e: + proxy.connect(gateway.ip, gateway.port, + username=gateway.username, + password=gateway.password, + pkey=gateway.private_key_obj) + except(paramiko.AuthenticationException, + paramiko.BadAuthenticationType, + SSHException) as e: return False, str(e) + sock = proxy.get_transport().open_channel( + 'direct-tcpip', ('127.0.0.1', gateway.port), ('127.0.0.1', 0) + ) + try: client.connect("127.0.0.1", port=gateway.port, username=gateway.username, diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index e6a353373..be6528219 100644 --- a/apps/assets/views/domain.py +++ b/apps/assets/views/domain.py @@ -140,11 +140,6 @@ class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView): domain = self.object.domain return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id}) - def form_valid(self, form): - response = super().form_valid(form) - print(form.cleaned_data) - return response - def get_context_data(self, **kwargs): context = { 'app': _('Assets'), diff --git a/apps/common/api.py b/apps/common/api.py index 4b74b6b0d..63ed9723d 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -21,23 +21,13 @@ class MailTestingAPI(APIView): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): email_host_user = serializer.validated_data["EMAIL_HOST_USER"] - kwargs = { - "host": serializer.validated_data["EMAIL_HOST"], - "port": serializer.validated_data["EMAIL_PORT"], - "username": serializer.validated_data["EMAIL_HOST_USER"], - "password": serializer.validated_data["EMAIL_HOST_PASSWORD"], - "use_ssl": serializer.validated_data["EMAIL_USE_SSL"], - "use_tls": serializer.validated_data["EMAIL_USE_TLS"] - } - connection = get_connection(timeout=5, **kwargs) + for k, v in serializer.validated_data.items(): + if k.startswith('EMAIL'): + setattr(settings, k, v) try: - connection.open() - except Exception as e: - return Response({"error": str(e)}, status=401) - - try: - send_mail("Test", "Test smtp setting", email_host_user, - [email_host_user], connection=connection) + subject = "Test" + message = "Test smtp setting" + send_mail(subject, message, email_host_user, [email_host_user]) except Exception as e: return Response({"error": str(e)}, status=401) @@ -96,14 +86,7 @@ class LDAPTestingAPI(APIView): class DjangoSettingsAPI(APIView): def get(self, request): - if not settings.DEBUG: - return Response('Only debug mode support') - - configs = {} - for i in dir(settings): - if i.isupper(): - configs[i] = str(getattr(settings, i)) - return Response(configs) + return Response('Danger, Close now') diff --git a/apps/common/forms.py b/apps/common/forms.py index 02af6e47a..a11420498 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -168,3 +168,63 @@ class TerminalSettingForm(BaseForm): ) ) + +class SecuritySettingForm(BaseForm): + # MFA global setting + SECURITY_MFA_AUTH = forms.BooleanField( + initial=False, required=False, + label=_("MFA Secondary certification"), + help_text=_( + 'After opening, the user login must use MFA secondary ' + 'authentication (valid for all users, including administrators)' + ) + ) + # limit login count + SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( + initial=3, min_value=3, + label=_("Limit the number of login failures") + ) + # limit login time + SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( + initial=30, min_value=5, + label=_("No logon interval"), + help_text=_( + "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." + ) + ) + # min length + SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( + initial=6, label=_("Password minimum length"), + min_value=6 + ) + # upper case + SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( + + initial=False, required=False, + label=_("Must contain capital letters"), + help_text=_( + 'After opening, the user password changes ' + 'and resets must contain uppercase letters') + ) + # lower case + SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField( + initial=False, required=False, + label=_("Must contain lowercase letters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain lowercase letters') + ) + # number + SECURITY_PASSWORD_NUMBER = forms.BooleanField( + initial=False, required=False, + label=_("Must contain numeric characters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain numeric characters') + ) + # special char + SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField( + initial=False, required=False, + label=_("Must contain special characters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain special characters') + ) diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py index df2ea5a5e..bff3a2193 100644 --- a/apps/common/signals_handler.py +++ b/apps/common/signals_handler.py @@ -34,7 +34,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): def ldap_auth_on_changed(sender, enabled=True, **kwargs): if enabled: logger.debug("Enable LDAP auth") - if settings.AUTH_LDAP_BACKEND not in settings.AUTH_LDAP_BACKEND: + if settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) else: diff --git a/apps/common/tasks.py b/apps/common/tasks.py index dec738921..bfb005511 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -2,6 +2,7 @@ from django.core.mail import send_mail from django.conf import settings from celery import shared_task from .utils import get_logger +from .models import Setting logger = get_logger(__file__) @@ -21,6 +22,10 @@ def send_mail_async(*args, **kwargs): Example: send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None) """ + configs = Setting.objects.filter(name__startswith='EMAIL') + for config in configs: + setattr(settings, config.name, config.cleaned_value) + if len(args) == 3: args = list(args) args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0] diff --git a/apps/common/templates/common/basic_setting.html b/apps/common/templates/common/basic_setting.html index 496eca977..9c9258e33 100644 --- a/apps/common/templates/common/basic_setting.html +++ b/apps/common/templates/common/basic_setting.html @@ -23,6 +23,9 @@
  • {% trans 'Terminal setting' %}
  • +
  • + {% trans 'Security setting' %} +
  • diff --git a/apps/common/templates/common/email_setting.html b/apps/common/templates/common/email_setting.html index 1fd772db1..2f0951e00 100644 --- a/apps/common/templates/common/email_setting.html +++ b/apps/common/templates/common/email_setting.html @@ -23,6 +23,9 @@
  • {% trans 'Terminal setting' %}
  • +
  • + {% trans 'Security setting' %} +
  • diff --git a/apps/common/templates/common/ldap_setting.html b/apps/common/templates/common/ldap_setting.html index f0569f873..e55da5a8f 100644 --- a/apps/common/templates/common/ldap_setting.html +++ b/apps/common/templates/common/ldap_setting.html @@ -23,6 +23,9 @@
  • {% trans 'Terminal setting' %}
  • +
  • + {% trans 'Security setting' %} +
  • diff --git a/apps/common/templates/common/security_setting.html b/apps/common/templates/common/security_setting.html new file mode 100644 index 000000000..08d978d23 --- /dev/null +++ b/apps/common/templates/common/security_setting.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} + +

    {% trans "User login settings" %}

    + {% for field in form %} + {% if forloop.counter == 4 %} +
    +

    {% trans "Password check rule" %}

    + {% endif %} + + {% if not field.field|is_bool_field %} + {% bootstrap_field field layout="horizontal" %} + {% else %} +
    + +
    +
    + {{ field }} +
    +
    + {{ field.help_text }} +
    +
    +
    + {% endif %} + {% endfor %} + +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/common/templates/common/terminal_setting.html b/apps/common/templates/common/terminal_setting.html index 16927c05a..320f628b0 100644 --- a/apps/common/templates/common/terminal_setting.html +++ b/apps/common/templates/common/terminal_setting.html @@ -27,6 +27,9 @@ {% trans 'Terminal setting' %} +
  • + {% trans 'Security setting' %} +
  • @@ -39,6 +42,7 @@
    {% endif %} {% csrf_token %} +

    {% trans "Basic setting" %}

    {% for field in form %} {% if not field.field|is_bool_field %} @@ -60,6 +64,7 @@ {% endfor %}
    +

    {% trans "Command storage" %}

    {% trans 'Auto push' %}: @@ -130,8 +134,8 @@
    {% trans 'Push system user now' %}: @@ -139,8 +143,8 @@
    {% trans 'Test assets connective' %}: @@ -149,6 +153,15 @@
    {% trans 'Clear auth' %}: + + + +
    {% trans 'Change auth period' %}:#} @@ -236,6 +249,10 @@ function updateSystemUserNode(nodes) { } jumpserver.nodes_selected = {}; $(document).ready(function () { + if($('#id_protocol_type').text() === 'rdp'){ + $('.only-ssh').addClass('hidden') + } + $(".panel-body .table tr:visible:first").addClass('no-borders-tr'); $('.select2').select2() .on('select2:select', function(evt) { var data = evt.params.data; @@ -296,7 +313,7 @@ $(document).ready(function () { var success = function (data) { 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') + window.open(url, '', 'width=800,height=600,left=400,top=400') }; APIUpdateAttr({ url: the_url, @@ -318,6 +335,25 @@ $(document).ready(function () { success: success, flash_message: false }); +}).on('click', '.btn-clear-auth', function () { + var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}'; + var name = '{{ system_user.name }}'; + swal({ + title: '你确定清除该系统用户的认证信息吗 ?', + text: " [" + name + "] ", + type: "warning", + showCancelButton: true, + cancelButtonText: '取消', + confirmButtonColor: "#ed5565", + confirmButtonText: '确认', + closeOnConfirm: true + }, function () { + APIUpdateAttr({ + url: the_url, + method: 'DELETE', + success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}" + }); + }); }) {% endblock %} diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 3daa47a81..3fa887df6 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -26,6 +26,7 @@ {% trans 'Name' %} {% trans 'Username' %} {% trans 'Protocol' %}{% trans 'Login mode' %} {% trans 'Asset' %} {% trans 'Reachable' %} {% trans 'Unreachable' %}
    diff --git a/apps/common/urls/view_urls.py b/apps/common/urls/view_urls.py index 466f7c49c..e7ccddd06 100644 --- a/apps/common/urls/view_urls.py +++ b/apps/common/urls/view_urls.py @@ -11,4 +11,5 @@ urlpatterns = [ url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'), + url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'), ] diff --git a/apps/common/utils.py b/apps/common/utils.py index d73c094ec..deaeb5280 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -16,6 +16,7 @@ import calendar import threading from io import StringIO import uuid +from functools import wraps import paramiko import sshpubkeys @@ -395,3 +396,17 @@ class TeeObj: def close(self): self.file_obj.close() + +def with_cache(func): + cache = {} + key = "_{}.{}".format(func.__module__, func.__name__) + + @wraps(func) + def wrapper(*args, **kwargs): + cached = cache.get(key) + if cached: + return cached + res = func(*args, **kwargs) + cache[key] = res + return res + return wrapper diff --git a/apps/common/validators.py b/apps/common/validators.py new file mode 100644 index 000000000..b273bd1de --- /dev/null +++ b/apps/common/validators.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +from django.core.validators import RegexValidator +from django.utils.translation import ugettext_lazy as _ + + +alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed')) \ No newline at end of file diff --git a/apps/common/views.py b/apps/common/views.py index ee7a2225f..6a7d37f49 100644 --- a/apps/common/views.py +++ b/apps/common/views.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _ from django.conf import settings from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ - TerminalSettingForm + TerminalSettingForm, SecuritySettingForm from .mixins import AdminUserRequiredMixin from .signals import ldap_auth_enable @@ -82,7 +82,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView): if form.is_valid(): form.save() if "AUTH_LDAP" in form.cleaned_data: - ldap_auth_enable.send(form.cleaned_data["AUTH_LDAP"]) + ldap_auth_enable.send(sender=self.__class__, enabled=form.cleaned_data["AUTH_LDAP"]) msg = _("Update setting successfully, please restart program") messages.success(request, msg) return redirect('settings:ldap-setting') @@ -122,3 +122,27 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView): return render(request, self.template_name, context) +class SecuritySettingView(AdminUserRequiredMixin, TemplateView): + form_class = SecuritySettingForm + template_name = "common/security_setting.html" + + def get_context_data(self, **kwargs): + context = { + 'app': _('Settings'), + 'action': _('Security setting'), + 'form': self.form_class(), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def post(self, request): + form = self.form_class(request.POST) + if form.is_valid(): + form.save() + msg = _("Update setting successfully, please restart program") + messages.success(request, msg) + return redirect('settings:security-setting') + else: + context = self.get_context_data() + context.update({"form": form}) + return render(request, self.template_name, context) diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo index ebfffbfe7..78d56177e 100644 Binary files a/apps/i18n/zh/LC_MESSAGES/django.mo and b/apps/i18n/zh/LC_MESSAGES/django.mo differ diff --git a/apps/i18n/zh/LC_MESSAGES/django.po b/apps/i18n/zh/LC_MESSAGES/django.po index 627e44ed6..0cf576ebe 100644 --- a/apps/i18n/zh/LC_MESSAGES/django.po +++ b/apps/i18n/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: 2018-04-23 19:51+0800\n" +"POT-Creation-Date: 2018-07-06 13:11+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -17,51 +17,51 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: assets/api/node.py:88 +#: assets/api/node.py:99 msgid "New node {}" msgstr "新节点 {}" -#: assets/api/node.py:216 +#: assets/api/node.py:234 msgid "更新节点资产硬件信息: {}" msgstr "" -#: assets/api/node.py:229 +#: assets/api/node.py:247 msgid "测试节点下资产是否可连接: {}" msgstr "" -#: assets/forms/asset.py:24 assets/models/asset.py:54 assets/models/user.py:103 +#: assets/forms/asset.py:24 assets/models/asset.py:89 assets/models/user.py:112 #: assets/templates/assets/asset_detail.html:183 #: assets/templates/assets/asset_detail.html:191 -#: assets/templates/assets/system_user_detail.html:166 perms/models.py:23 +#: assets/templates/assets/system_user_detail.html:179 perms/models.py:33 msgid "Nodes" msgstr "节点管理" #: assets/forms/asset.py:27 assets/forms/asset.py:66 assets/forms/asset.py:109 -#: assets/forms/asset.py:113 assets/models/asset.py:58 +#: assets/forms/asset.py:113 assets/models/asset.py:94 #: assets/models/cluster.py:19 assets/models/user.py:72 #: assets/templates/assets/asset_detail.html:73 templates/_nav.html:25 msgid "Admin user" msgstr "管理用户" #: assets/forms/asset.py:30 assets/forms/asset.py:69 assets/forms/asset.py:125 -#: assets/templates/assets/asset_create.html:35 -#: assets/templates/assets/asset_create.html:37 +#: assets/templates/assets/asset_create.html:36 +#: assets/templates/assets/asset_create.html:38 #: assets/templates/assets/asset_list.html:75 -#: assets/templates/assets/asset_update.html:40 -#: assets/templates/assets/asset_update.html:42 +#: assets/templates/assets/asset_update.html:41 +#: assets/templates/assets/asset_update.html:43 #: assets/templates/assets/user_asset_list.html:34 msgid "Label" msgstr "标签" -#: assets/forms/asset.py:34 assets/forms/asset.py:73 assets/models/asset.py:53 +#: assets/forms/asset.py:34 assets/forms/asset.py:73 assets/models/asset.py:85 #: assets/models/domain.py:46 msgid "Domain" msgstr "网域" #: assets/forms/asset.py:38 assets/forms/asset.py:63 assets/forms/asset.py:77 -#: assets/forms/asset.py:128 assets/templates/assets/asset_create.html:29 -#: assets/templates/assets/asset_update.html:34 perms/forms.py:40 -#: perms/forms.py:47 perms/models.py:67 +#: assets/forms/asset.py:128 assets/templates/assets/asset_create.html:30 +#: assets/templates/assets/asset_update.html:35 perms/forms.py:40 +#: perms/forms.py:47 perms/models.py:76 #: perms/templates/perms/asset_permission_list.html:57 #: perms/templates/perms/asset_permission_list.html:142 msgid "Node" @@ -90,7 +90,7 @@ msgstr "如果有多个的互相隔离的网络,设置资产属于的网域, msgid "Select assets" msgstr "选择资产" -#: assets/forms/asset.py:105 assets/models/asset.py:51 +#: assets/forms/asset.py:105 assets/models/asset.py:81 #: assets/models/domain.py:44 assets/templates/assets/admin_user_assets.html:53 #: assets/templates/assets/asset_detail.html:69 #: assets/templates/assets/domain_gateway_list.html:58 @@ -99,18 +99,18 @@ msgid "Port" msgstr "端口" #: assets/forms/domain.py:14 assets/forms/label.py:13 -#: assets/models/asset.py:169 assets/templates/assets/admin_user_list.html:25 +#: assets/models/asset.py:237 assets/templates/assets/admin_user_list.html:25 #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:15 #: assets/templates/assets/label_list.html:16 -#: assets/templates/assets/system_user_list.html:29 audits/models.py:11 +#: assets/templates/assets/system_user_list.html:30 audits/models.py:11 #: audits/templates/audits/ftp_log_list.html:41 #: audits/templates/audits/ftp_log_list.html:72 perms/forms.py:37 -#: perms/models.py:22 +#: perms/models.py:32 #: perms/templates/perms/asset_permission_create_update.html:40 #: perms/templates/perms/asset_permission_list.html:56 #: perms/templates/perms/asset_permission_list.html:139 -#: terminal/backends/command/models.py:11 terminal/models.py:123 +#: terminal/backends/command/models.py:11 terminal/models.py:127 #: terminal/templates/terminal/command_list.html:40 #: terminal/templates/terminal/command_list.html:73 #: terminal/templates/terminal/session_list.html:41 @@ -118,8 +118,8 @@ msgstr "端口" msgid "Asset" msgstr "资产" -#: assets/forms/domain.py:54 assets/forms/user.py:79 assets/forms/user.py:120 -#: assets/models/base.py:20 assets/models/cluster.py:18 +#: assets/forms/domain.py:54 assets/forms/user.py:79 assets/forms/user.py:139 +#: assets/models/base.py:21 assets/models/cluster.py:18 #: assets/models/domain.py:17 assets/models/group.py:20 #: assets/models/label.py:17 assets/templates/assets/admin_user_detail.html:56 #: assets/templates/assets/admin_user_list.html:23 @@ -129,15 +129,15 @@ msgstr "资产" #: assets/templates/assets/label_list.html:14 #: assets/templates/assets/system_user_detail.html:58 #: assets/templates/assets/system_user_list.html:26 common/models.py:26 -#: common/templates/common/terminal_setting.html:67 -#: common/templates/common/terminal_setting.html:85 ops/models/adhoc.py:36 +#: common/templates/common/terminal_setting.html:72 +#: common/templates/common/terminal_setting.html:90 ops/models/adhoc.py:36 #: ops/templates/ops/task_detail.html:59 ops/templates/ops/task_list.html:35 -#: perms/models.py:19 perms/templates/perms/asset_permission_detail.html:62 +#: perms/models.py:29 perms/templates/perms/asset_permission_detail.html:62 #: perms/templates/perms/asset_permission_list.html:53 #: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:16 -#: terminal/models.py:149 terminal/templates/terminal/terminal_detail.html:43 -#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:42 users/templates/users/_select_user_modal.html:13 +#: terminal/models.py:154 terminal/templates/terminal/terminal_detail.html:43 +#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:12 +#: users/models/user.py:49 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:12 @@ -147,16 +147,16 @@ msgstr "资产" msgid "Name" msgstr "名称" -#: assets/forms/domain.py:55 assets/forms/user.py:80 assets/forms/user.py:121 -#: assets/models/base.py:21 assets/templates/assets/admin_user_detail.html:60 +#: assets/forms/domain.py:55 assets/forms/user.py:80 assets/forms/user.py:140 +#: assets/models/base.py:22 assets/templates/assets/admin_user_detail.html:60 #: assets/templates/assets/admin_user_list.html:24 #: assets/templates/assets/domain_gateway_list.html:60 #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:27 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:13 -#: users/forms.py:22 users/models/authentication.py:45 users/models/user.py:40 +#: users/forms.py:31 users/models/authentication.py:70 users/models/user.py:47 #: users/templates/users/_select_user_modal.html:14 -#: users/templates/users/login.html:56 +#: users/templates/users/login.html:60 #: users/templates/users/login_log_list.html:49 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:24 @@ -168,19 +168,19 @@ msgstr "用户名" msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:25 assets/models/base.py:22 common/forms.py:113 -#: users/forms.py:15 users/forms.py:24 users/forms.py:36 -#: users/templates/users/login.html:59 -#: users/templates/users/reset_password.html:52 +#: assets/forms/user.py:25 assets/models/base.py:23 common/forms.py:113 +#: users/forms.py:15 users/forms.py:33 users/forms.py:45 +#: users/templates/users/login.html:63 +#: users/templates/users/reset_password.html:53 #: users/templates/users/user_create.html:10 #: users/templates/users/user_password_authentication.html:14 -#: users/templates/users/user_password_update.html:40 +#: users/templates/users/user_password_update.html:42 #: users/templates/users/user_profile_update.html:40 #: users/templates/users/user_pubkey_update.html:40 msgid "Password" msgstr "密码" -#: assets/forms/user.py:28 users/models/user.py:69 +#: assets/forms/user.py:28 users/models/user.py:76 msgid "Private key" msgstr "ssh私钥" @@ -192,17 +192,27 @@ msgstr "ssh密钥不合法" msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms/user.py:126 +#: assets/forms/user.py:125 +msgid "* Automatic login mode, must fill in the username." +msgstr "自动登录模式,必须填写用户名" + +#: assets/forms/user.py:145 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms/user.py:127 +#: assets/forms/user.py:146 msgid "" "High level will be using login asset as default, if user was granted more " "than 2 system user" msgstr "高优先级的系统用户将会作为默认登录用户" -#: assets/models/asset.py:49 assets/models/domain.py:43 +#: assets/forms/user.py:148 +msgid "" +"If you choose manual login mode, you do not need to fill in the username and " +"password." +msgstr "如果选择手动登录模式,用户名和密码则不需要填写" + +#: assets/models/asset.py:74 assets/models/domain.py:43 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/admin_user_assets.html:52 #: assets/templates/assets/asset_detail.html:61 @@ -217,7 +227,7 @@ msgstr "高优先级的系统用户将会作为默认登录用户" msgid "IP" msgstr "IP" -#: assets/models/asset.py:50 assets/templates/assets/_asset_list_modal.html:45 +#: assets/models/asset.py:77 assets/templates/assets/_asset_list_modal.html:45 #: assets/templates/assets/admin_user_assets.html:51 #: assets/templates/assets/asset_detail.html:57 #: assets/templates/assets/asset_list.html:86 @@ -229,107 +239,116 @@ msgstr "IP" msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:52 assets/templates/assets/asset_detail.html:97 +#: assets/models/asset.py:80 assets/models/domain.py:45 +#: assets/models/user.py:115 +#: assets/templates/assets/domain_gateway_list.html:59 +#: assets/templates/assets/system_user_detail.html:70 +#: assets/templates/assets/system_user_list.html:28 +#: terminal/templates/terminal/session_list.html:75 +msgid "Protocol" +msgstr "协议" + +#: assets/models/asset.py:83 assets/templates/assets/asset_detail.html:97 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:55 assets/models/domain.py:48 +#: assets/models/asset.py:90 assets/models/domain.py:48 #: assets/models/label.py:20 assets/templates/assets/asset_detail.html:105 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:61 assets/templates/assets/asset_detail.html:65 +#: assets/models/asset.py:99 assets/templates/assets/asset_detail.html:65 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:62 assets/templates/assets/asset_detail.html:113 +#: assets/models/asset.py:101 assets/templates/assets/asset_detail.html:113 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:65 assets/templates/assets/asset_detail.html:77 +#: assets/models/asset.py:105 assets/templates/assets/asset_detail.html:77 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:66 assets/templates/assets/asset_detail.html:81 +#: assets/models/asset.py:107 assets/templates/assets/asset_detail.html:81 msgid "Model" msgstr "型号" -#: assets/models/asset.py:67 assets/templates/assets/asset_detail.html:109 +#: assets/models/asset.py:109 assets/templates/assets/asset_detail.html:109 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:69 +#: assets/models/asset.py:112 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:70 +#: assets/models/asset.py:113 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:71 +#: assets/models/asset.py:114 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:72 assets/templates/assets/asset_detail.html:89 +#: assets/models/asset.py:116 assets/templates/assets/asset_detail.html:89 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:73 +#: assets/models/asset.py:118 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:74 +#: assets/models/asset.py:120 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:76 assets/templates/assets/asset_detail.html:101 +#: assets/models/asset.py:123 assets/templates/assets/asset_detail.html:101 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:77 +#: assets/models/asset.py:125 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:78 +#: assets/models/asset.py:127 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:79 +#: assets/models/asset.py:129 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:81 assets/templates/assets/asset_create.html:33 +#: assets/models/asset.py:133 assets/templates/assets/asset_create.html:34 #: assets/templates/assets/asset_detail.html:220 -#: assets/templates/assets/asset_update.html:38 templates/_nav.html:27 +#: assets/templates/assets/asset_update.html:39 templates/_nav.html:27 msgid "Labels" msgstr "标签管理" -#: assets/models/asset.py:82 assets/models/base.py:28 +#: assets/models/asset.py:135 assets/models/base.py:29 #: assets/models/cluster.py:28 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:68 #: assets/templates/assets/asset_detail.html:117 #: assets/templates/assets/domain_detail.html:72 -#: assets/templates/assets/system_user_detail.html:96 -#: ops/templates/ops/adhoc_detail.html:86 perms/models.py:28 perms/models.py:72 +#: assets/templates/assets/system_user_detail.html:100 +#: ops/templates/ops/adhoc_detail.html:86 perms/models.py:38 perms/models.py:81 #: perms/templates/perms/asset_permission_detail.html:98 -#: users/models/user.py:83 users/templates/users/user_detail.html:107 +#: users/models/user.py:90 users/templates/users/user_detail.html:111 msgid "Created by" msgstr "创建者" -#: assets/models/asset.py:83 assets/models/cluster.py:26 +#: assets/models/asset.py:138 assets/models/cluster.py:26 #: assets/models/domain.py:20 assets/models/group.py:22 #: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:64 #: assets/templates/assets/domain_detail.html:68 -#: assets/templates/assets/system_user_detail.html:92 +#: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:63 -#: perms/models.py:29 perms/models.py:73 +#: perms/models.py:39 perms/models.py:82 #: perms/templates/perms/asset_permission_detail.html:94 -#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 +#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:15 #: users/templates/users/user_group_detail.html:63 msgid "Date created" msgstr "创建日期" -#: assets/models/asset.py:84 assets/models/base.py:25 +#: assets/models/asset.py:140 assets/models/base.py:26 #: assets/models/cluster.py:29 assets/models/domain.py:18 #: assets/models/domain.py:47 assets/models/group.py:23 #: assets/models/label.py:21 assets/templates/assets/admin_user_detail.html:72 @@ -338,23 +357,23 @@ msgstr "创建日期" #: assets/templates/assets/domain_detail.html:76 #: assets/templates/assets/domain_gateway_list.html:61 #: assets/templates/assets/domain_list.html:17 -#: assets/templates/assets/system_user_detail.html:100 -#: assets/templates/assets/system_user_list.html:33 common/models.py:30 -#: ops/models/adhoc.py:42 perms/models.py:30 perms/models.py:74 +#: assets/templates/assets/system_user_detail.html:104 +#: assets/templates/assets/system_user_list.html:34 common/models.py:30 +#: ops/models/adhoc.py:42 perms/models.py:40 perms/models.py:83 #: perms/templates/perms/asset_permission_detail.html:102 terminal/models.py:26 -#: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:75 users/templates/users/user_detail.html:119 +#: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:13 +#: users/models/user.py:82 users/templates/users/user_detail.html:123 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_profile.html:123 +#: users/templates/users/user_profile.html:130 msgid "Comment" msgstr "备注" -#: assets/models/base.py:23 +#: assets/models/base.py:24 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:24 +#: assets/models/base.py:25 msgid "SSH public key" msgstr "ssh公钥" @@ -366,7 +385,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:61 +#: assets/models/cluster.py:22 users/models/user.py:68 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -392,7 +411,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:13 -#: users/models/user.py:330 +#: users/models/user.py:345 msgid "System" msgstr "系统" @@ -404,13 +423,6 @@ msgstr "默认Cluster" msgid "Cluster" msgstr "集群" -#: assets/models/domain.py:45 assets/models/user.py:106 -#: assets/templates/assets/domain_gateway_list.html:59 -#: assets/templates/assets/system_user_detail.html:66 -#: assets/templates/assets/system_user_list.html:28 -msgid "Protocol" -msgstr "协议" - #: assets/models/group.py:30 msgid "Asset group" msgstr "资产组" @@ -422,22 +434,22 @@ msgstr "默认资产组" #: assets/models/label.py:14 audits/models.py:9 #: audits/templates/audits/ftp_log_list.html:33 #: audits/templates/audits/ftp_log_list.html:71 perms/forms.py:14 -#: perms/forms.py:31 perms/models.py:20 +#: perms/forms.py:31 perms/models.py:30 #: perms/templates/perms/asset_permission_create_update.html:36 #: perms/templates/perms/asset_permission_list.html:54 #: perms/templates/perms/asset_permission_list.html:133 -#: terminal/backends/command/models.py:10 terminal/models.py:122 +#: terminal/backends/command/models.py:10 terminal/models.py:126 #: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:273 -#: users/models/user.py:30 users/models/user.py:318 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:282 +#: users/models/user.py:31 users/models/user.py:333 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:13 users/views/user.py:339 +#: users/templates/users/user_group_list.html:13 users/views/user.py:361 msgid "User" msgstr "用户" -#: assets/models/label.py:18 assets/models/node.py:15 +#: assets/models/label.py:18 assets/models/node.py:16 #: assets/templates/assets/label_list.html:15 common/models.py:27 msgid "Value" msgstr "值" @@ -446,11 +458,19 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:14 +#: assets/models/node.py:15 msgid "Key" msgstr "" -#: assets/models/user.py:104 +#: assets/models/user.py:108 +msgid "Automatic login" +msgstr "自动登录" + +#: assets/models/user.py:109 +msgid "Manually login" +msgstr "手动登录" + +#: assets/models/user.py:113 #: assets/templates/assets/_asset_group_bulk_update_modal.html:11 #: assets/templates/assets/system_user_asset.html:21 #: assets/views/admin_user.py:29 assets/views/admin_user.py:47 @@ -460,7 +480,7 @@ msgstr "" #: assets/views/asset.py:197 assets/views/domain.py:29 #: assets/views/domain.py:45 assets/views/domain.py:61 #: assets/views/domain.py:74 assets/views/domain.py:98 -#: assets/views/domain.py:126 assets/views/domain.py:150 +#: assets/views/domain.py:126 assets/views/domain.py:145 #: assets/views/label.py:26 assets/views/label.py:42 assets/views/label.py:58 #: assets/views/system_user.py:28 assets/views/system_user.py:44 #: assets/views/system_user.py:60 assets/views/system_user.py:74 @@ -468,32 +488,37 @@ msgstr "" msgid "Assets" msgstr "资产管理" -#: assets/models/user.py:105 +#: assets/models/user.py:114 msgid "Priority" msgstr "优先级" -#: assets/models/user.py:107 assets/templates/assets/_system_user.html:58 -#: assets/templates/assets/system_user_detail.html:118 -#: assets/templates/assets/system_user_update.html:11 +#: assets/models/user.py:116 assets/templates/assets/_system_user.html:59 +#: assets/templates/assets/system_user_detail.html:122 +#: assets/templates/assets/system_user_update.html:10 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:108 assets/templates/assets/system_user_detail.html:70 +#: assets/models/user.py:117 assets/templates/assets/system_user_detail.html:74 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:109 assets/templates/assets/system_user_detail.html:75 +#: assets/models/user.py:118 assets/templates/assets/system_user_detail.html:79 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:149 audits/models.py:12 +#: assets/models/user.py:119 assets/templates/assets/system_user_detail.html:66 +#: assets/templates/assets/system_user_list.html:29 +msgid "Login mode" +msgstr "登录模式" + +#: assets/models/user.py:159 audits/models.py:12 #: audits/templates/audits/ftp_log_list.html:49 #: audits/templates/audits/ftp_log_list.html:73 perms/forms.py:43 -#: perms/models.py:24 perms/models.py:69 +#: perms/models.py:34 perms/models.py:78 #: perms/templates/perms/asset_permission_detail.html:140 #: perms/templates/perms/asset_permission_list.html:58 #: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:26 -#: terminal/backends/command/models.py:12 terminal/models.py:124 +#: terminal/backends/command/models.py:12 terminal/models.py:128 #: terminal/templates/terminal/command_list.html:48 #: terminal/templates/terminal/command_list.html:74 #: terminal/templates/terminal/session_list.html:49 @@ -561,7 +586,6 @@ msgid "Select System Users" msgstr "选择系统用户" #: assets/templates/assets/_asset_group_bulk_update_modal.html:34 -#, fuzzy msgid "Enable-MFA" msgstr "启用MFA" @@ -601,72 +625,73 @@ msgid "Basic" msgstr "基本" #: assets/templates/assets/_system_user.html:44 -#: assets/templates/assets/asset_create.html:25 -#: assets/templates/assets/asset_update.html:30 +#: assets/templates/assets/asset_create.html:26 +#: assets/templates/assets/asset_update.html:31 #: assets/templates/assets/gateway_create_update.html:45 -#: assets/templates/assets/system_user_update.html:7 #: users/templates/users/_user.html:21 msgid "Auth" msgstr "认证" -#: assets/templates/assets/_system_user.html:47 +#: assets/templates/assets/_system_user.html:48 msgid "Auto generate key" msgstr "自动生成密钥" -#: assets/templates/assets/_system_user.html:64 -#: assets/templates/assets/asset_create.html:59 -#: assets/templates/assets/asset_update.html:63 +#: assets/templates/assets/_system_user.html:65 +#: assets/templates/assets/asset_create.html:60 +#: assets/templates/assets/asset_update.html:64 #: assets/templates/assets/gateway_create_update.html:53 #: perms/templates/perms/asset_permission_create_update.html:45 #: terminal/templates/terminal/terminal_update.html:42 msgid "Other" msgstr "其它" -#: assets/templates/assets/_system_user.html:70 +#: assets/templates/assets/_system_user.html:71 #: assets/templates/assets/admin_user_create_update.html:45 #: assets/templates/assets/asset_bulk_update.html:23 -#: assets/templates/assets/asset_create.html:66 -#: assets/templates/assets/asset_update.html:70 +#: assets/templates/assets/asset_create.html:67 +#: assets/templates/assets/asset_update.html:71 #: assets/templates/assets/domain_create_update.html:16 #: assets/templates/assets/gateway_create_update.html:58 #: assets/templates/assets/label_create_update.html:18 -#: common/templates/common/basic_setting.html:58 -#: common/templates/common/email_setting.html:59 -#: common/templates/common/ldap_setting.html:59 -#: common/templates/common/terminal_setting.html:101 +#: common/templates/common/basic_setting.html:61 +#: common/templates/common/email_setting.html:62 +#: common/templates/common/ldap_setting.html:62 +#: common/templates/common/security_setting.html:70 +#: common/templates/common/terminal_setting.html:106 #: perms/templates/perms/asset_permission_create_update.html:69 #: terminal/templates/terminal/terminal_update.html:47 #: users/templates/users/_user.html:46 #: users/templates/users/user_bulk_update.html:23 -#: users/templates/users/user_password_update.html:58 -#: users/templates/users/user_profile.html:181 +#: users/templates/users/user_password_update.html:70 +#: users/templates/users/user_profile.html:188 #: users/templates/users/user_profile_update.html:63 #: users/templates/users/user_pubkey_update.html:70 #: users/templates/users/user_pubkey_update.html:76 msgid "Reset" msgstr "重置" -#: assets/templates/assets/_system_user.html:71 +#: assets/templates/assets/_system_user.html:72 #: assets/templates/assets/admin_user_create_update.html:46 #: assets/templates/assets/asset_bulk_update.html:24 -#: assets/templates/assets/asset_create.html:67 +#: assets/templates/assets/asset_create.html:68 #: assets/templates/assets/asset_list.html:108 -#: assets/templates/assets/asset_update.html:71 +#: assets/templates/assets/asset_update.html:72 #: assets/templates/assets/domain_create_update.html:17 #: assets/templates/assets/gateway_create_update.html:59 #: assets/templates/assets/label_create_update.html:19 -#: common/templates/common/basic_setting.html:59 -#: common/templates/common/email_setting.html:60 -#: common/templates/common/ldap_setting.html:60 -#: common/templates/common/terminal_setting.html:103 +#: common/templates/common/basic_setting.html:62 +#: common/templates/common/email_setting.html:63 +#: common/templates/common/ldap_setting.html:63 +#: common/templates/common/security_setting.html:71 +#: common/templates/common/terminal_setting.html:108 #: perms/templates/perms/asset_permission_create_update.html:70 -#: terminal/templates/terminal/session_list.html:120 +#: terminal/templates/terminal/session_list.html:126 #: terminal/templates/terminal/terminal_update.html:48 #: users/templates/users/_user.html:47 #: users/templates/users/forgot_password.html:44 #: users/templates/users/user_bulk_update.html:24 -#: users/templates/users/user_list.html:44 -#: users/templates/users/user_password_update.html:59 +#: users/templates/users/user_list.html:45 +#: users/templates/users/user_password_update.html:71 #: users/templates/users/user_profile_update.html:64 #: users/templates/users/user_pubkey_update.html:77 msgid "Submit" @@ -699,16 +724,15 @@ msgstr "资产列表" #: assets/templates/assets/admin_user_assets.html:54 #: assets/templates/assets/admin_user_list.html:26 -#: assets/templates/assets/asset_list.html:90 #: assets/templates/assets/system_user_asset.html:52 -#: assets/templates/assets/system_user_list.html:30 +#: assets/templates/assets/system_user_list.html:31 #: users/templates/users/user_group_granted_asset.html:47 msgid "Reachable" msgstr "可连接" #: assets/templates/assets/admin_user_assets.html:66 #: assets/templates/assets/system_user_asset.html:64 -#: assets/templates/assets/system_user_detail.html:112 +#: assets/templates/assets/system_user_detail.html:116 #: perms/templates/perms/asset_permission_detail.html:114 msgid "Quick update" msgstr "快速更新" @@ -721,21 +745,21 @@ msgstr "测试可连接性" #: assets/templates/assets/admin_user_assets.html:75 #: assets/templates/assets/asset_detail.html:171 #: assets/templates/assets/system_user_asset.html:81 -#: assets/templates/assets/system_user_detail.html:147 +#: assets/templates/assets/system_user_detail.html:151 msgid "Test" msgstr "测试" #: assets/templates/assets/admin_user_detail.html:24 #: assets/templates/assets/admin_user_list.html:85 #: assets/templates/assets/asset_detail.html:24 -#: assets/templates/assets/asset_list.html:170 +#: assets/templates/assets/asset_list.html:175 #: assets/templates/assets/domain_detail.html:24 #: assets/templates/assets/domain_detail.html:103 #: assets/templates/assets/domain_gateway_list.html:85 #: assets/templates/assets/domain_list.html:42 #: assets/templates/assets/label_list.html:38 #: assets/templates/assets/system_user_detail.html:26 -#: assets/templates/assets/system_user_list.html:88 +#: assets/templates/assets/system_user_list.html:89 #: perms/templates/perms/asset_permission_detail.html:30 #: perms/templates/perms/asset_permission_list.html:191 #: terminal/templates/terminal/terminal_detail.html:16 @@ -743,23 +767,23 @@ msgstr "测试" #: users/templates/users/user_detail.html:25 #: users/templates/users/user_group_detail.html:28 #: users/templates/users/user_group_list.html:43 -#: users/templates/users/user_list.html:76 -#: users/templates/users/user_profile.html:144 -#: users/templates/users/user_profile.html:173 +#: users/templates/users/user_list.html:77 +#: users/templates/users/user_profile.html:151 +#: users/templates/users/user_profile.html:180 msgid "Update" msgstr "更新" #: assets/templates/assets/admin_user_detail.html:28 #: assets/templates/assets/admin_user_list.html:86 #: assets/templates/assets/asset_detail.html:28 -#: assets/templates/assets/asset_list.html:171 +#: assets/templates/assets/asset_list.html:176 #: assets/templates/assets/domain_detail.html:28 #: assets/templates/assets/domain_detail.html:104 #: assets/templates/assets/domain_gateway_list.html:86 #: assets/templates/assets/domain_list.html:43 #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:30 -#: assets/templates/assets/system_user_list.html:89 +#: assets/templates/assets/system_user_list.html:90 #: ops/templates/ops/task_list.html:72 #: perms/templates/perms/asset_permission_detail.html:34 #: perms/templates/perms/asset_permission_list.html:192 @@ -767,8 +791,8 @@ msgstr "更新" #: users/templates/users/user_detail.html:30 #: users/templates/users/user_group_detail.html:32 #: users/templates/users/user_group_list.html:45 -#: users/templates/users/user_list.html:80 -#: users/templates/users/user_list.html:84 +#: users/templates/users/user_list.html:81 +#: users/templates/users/user_list.html:85 msgid "Delete" msgstr "删除" @@ -783,17 +807,17 @@ msgstr "选择节点" #: assets/templates/assets/admin_user_detail.html:100 #: assets/templates/assets/asset_detail.html:200 -#: assets/templates/assets/asset_list.html:600 -#: assets/templates/assets/system_user_detail.html:183 -#: assets/templates/assets/system_user_list.html:138 templates/_modal.html:22 +#: assets/templates/assets/asset_list.html:638 +#: assets/templates/assets/system_user_detail.html:196 +#: assets/templates/assets/system_user_list.html:139 templates/_modal.html:22 #: terminal/templates/terminal/session_detail.html:108 -#: users/templates/users/user_detail.html:362 -#: users/templates/users/user_detail.html:387 -#: users/templates/users/user_detail.html:410 +#: users/templates/users/user_detail.html:366 +#: users/templates/users/user_detail.html:391 +#: users/templates/users/user_detail.html:414 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:86 -#: users/templates/users/user_list.html:196 -#: users/templates/users/user_profile.html:215 +#: users/templates/users/user_list.html:200 +#: users/templates/users/user_profile.html:222 msgid "Confirm" msgstr "确认" @@ -803,12 +827,12 @@ msgid "Create admin user" msgstr "创建管理用户" #: assets/templates/assets/admin_user_list.html:27 -#: assets/templates/assets/system_user_list.html:31 +#: assets/templates/assets/system_user_list.html:32 msgid "Unreachable" msgstr "不可达" #: assets/templates/assets/admin_user_list.html:28 -#: assets/templates/assets/system_user_list.html:32 +#: assets/templates/assets/system_user_list.html:33 #: ops/templates/ops/adhoc_history.html:54 #: ops/templates/ops/task_history.html:60 msgid "Ratio" @@ -819,14 +843,14 @@ msgstr "比例" #: assets/templates/assets/domain_gateway_list.html:62 #: assets/templates/assets/domain_list.html:18 #: assets/templates/assets/label_list.html:17 -#: assets/templates/assets/system_user_list.html:34 +#: assets/templates/assets/system_user_list.html:35 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 #: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42 #: perms/templates/perms/asset_permission_list.html:60 -#: terminal/templates/terminal/session_list.html:80 +#: terminal/templates/terminal/session_list.html:81 #: terminal/templates/terminal/terminal_list.html:36 #: users/templates/users/user_group_list.html:15 -#: users/templates/users/user_list.html:28 +#: users/templates/users/user_list.html:29 msgid "Action" msgstr "动作" @@ -843,31 +867,31 @@ msgid "Disk" msgstr "硬盘" #: assets/templates/assets/asset_detail.html:121 -#: users/templates/users/user_detail.html:111 -#: users/templates/users/user_profile.html:97 +#: users/templates/users/user_detail.html:115 +#: users/templates/users/user_profile.html:104 msgid "Date joined" msgstr "创建日期" #: assets/templates/assets/asset_detail.html:137 #: terminal/templates/terminal/session_detail.html:81 -#: users/templates/users/user_detail.html:130 -#: users/templates/users/user_profile.html:135 +#: users/templates/users/user_detail.html:134 +#: users/templates/users/user_profile.html:142 msgid "Quick modify" msgstr "快速修改" #: assets/templates/assets/asset_detail.html:143 #: assets/templates/assets/asset_list.html:89 -#: assets/templates/assets/user_asset_list.html:47 perms/models.py:25 -#: perms/models.py:70 +#: assets/templates/assets/user_asset_list.html:47 perms/models.py:35 +#: perms/models.py:79 #: perms/templates/perms/asset_permission_create_update.html:47 #: perms/templates/perms/asset_permission_detail.html:120 #: perms/templates/perms/asset_permission_list.html:59 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:136 +#: users/templates/users/user_detail.html:140 #: users/templates/users/user_granted_asset.html:46 #: users/templates/users/user_group_granted_asset.html:46 -#: users/templates/users/user_list.html:27 +#: users/templates/users/user_list.html:28 #: users/templates/users/user_profile.html:63 msgid "Active" msgstr "激活中" @@ -881,8 +905,8 @@ msgid "Refresh" msgstr "刷新" #: assets/templates/assets/asset_detail.html:300 -#: users/templates/users/user_detail.html:282 -#: users/templates/users/user_detail.html:309 +#: users/templates/users/user_detail.html:286 +#: users/templates/users/user_detail.html:313 msgid "Update successfully!" msgstr "更新成功" @@ -905,12 +929,12 @@ msgid "Hardware" msgstr "硬件" #: assets/templates/assets/asset_list.html:100 -#: users/templates/users/user_list.html:37 +#: users/templates/users/user_list.html:38 msgid "Delete selected" msgstr "批量删除" #: assets/templates/assets/asset_list.html:101 -#: users/templates/users/user_list.html:38 +#: users/templates/users/user_list.html:39 msgid "Update selected" msgstr "批量更新" @@ -919,12 +943,12 @@ msgid "Remove from this node" msgstr "从节点移除" #: assets/templates/assets/asset_list.html:103 -#: users/templates/users/user_list.html:39 +#: users/templates/users/user_list.html:40 msgid "Deactive selected" msgstr "禁用所选" #: assets/templates/assets/asset_list.html:104 -#: users/templates/users/user_list.html:40 +#: users/templates/users/user_list.html:41 msgid "Active selected" msgstr "激活所选" @@ -956,45 +980,53 @@ msgstr "更新节点资产硬件信息" msgid "Test node connective" msgstr "测试节点资产可连接性" -#: assets/templates/assets/asset_list.html:204 +#: assets/templates/assets/asset_list.html:131 +msgid "Display only current node assets" +msgstr "仅显示当前节点资产" + +#: assets/templates/assets/asset_list.html:132 +msgid "Displays all child node assets" +msgstr "显示所有子节点资产" + +#: assets/templates/assets/asset_list.html:218 msgid "Create node failed" msgstr "创建节点失败" -#: assets/templates/assets/asset_list.html:216 +#: assets/templates/assets/asset_list.html:230 msgid "Have child node, cancel" msgstr "存在子节点,不能删除" -#: assets/templates/assets/asset_list.html:218 +#: assets/templates/assets/asset_list.html:232 msgid "Have assets, cancel" msgstr "存在资产,不能删除" -#: assets/templates/assets/asset_list.html:595 -#: assets/templates/assets/system_user_list.html:133 -#: users/templates/users/user_detail.html:357 -#: users/templates/users/user_detail.html:382 +#: assets/templates/assets/asset_list.html:633 +#: assets/templates/assets/system_user_list.html:134 +#: users/templates/users/user_detail.html:361 +#: users/templates/users/user_detail.html:386 #: users/templates/users/user_group_list.html:81 -#: users/templates/users/user_list.html:191 +#: users/templates/users/user_list.html:195 msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_list.html:596 +#: assets/templates/assets/asset_list.html:634 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" -#: assets/templates/assets/asset_list.html:604 +#: assets/templates/assets/asset_list.html:642 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:605 -#: assets/templates/assets/asset_list.html:610 +#: assets/templates/assets/asset_list.html:643 +#: assets/templates/assets/asset_list.html:648 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:609 +#: assets/templates/assets/asset_list.html:647 msgid "Asset Deleting failed." msgstr "删除失败" -#: assets/templates/assets/asset_update.html:59 +#: assets/templates/assets/asset_update.html:60 msgid "Configuration" msgstr "配置" @@ -1025,8 +1057,9 @@ msgid "Create gateway" msgstr "创建网关" #: assets/templates/assets/domain_gateway_list.html:87 -#: common/templates/common/email_setting.html:58 -#: common/templates/common/ldap_setting.html:58 +#: assets/templates/assets/domain_gateway_list.html:89 +#: common/templates/common/email_setting.html:61 +#: common/templates/common/ldap_setting.html:61 msgid "Test connection" msgstr "测试连接" @@ -1043,17 +1076,17 @@ msgid "Assets of " msgstr "资产" #: assets/templates/assets/system_user_asset.html:70 -#: assets/templates/assets/system_user_detail.html:135 +#: assets/templates/assets/system_user_detail.html:139 msgid "Push system user now" msgstr "立刻推送系统" #: assets/templates/assets/system_user_asset.html:73 -#: assets/templates/assets/system_user_detail.html:138 +#: assets/templates/assets/system_user_detail.html:142 msgid "Push" msgstr "推送" #: assets/templates/assets/system_user_asset.html:78 -#: assets/templates/assets/system_user_detail.html:144 +#: assets/templates/assets/system_user_detail.html:148 msgid "Test assets connective" msgstr "测试资产可连接性" @@ -1065,37 +1098,50 @@ msgstr "任务已下发,查看ops任务列表" msgid "Task has been send, seen left assets status" msgstr "任务已下发,查看左侧资产状态" -#: assets/templates/assets/system_user_detail.html:81 +#: assets/templates/assets/system_user_detail.html:85 msgid "Home" msgstr "家目录" -#: assets/templates/assets/system_user_detail.html:87 +#: assets/templates/assets/system_user_detail.html:91 msgid "Uid" msgstr "Uid" -#: assets/templates/assets/system_user_detail.html:174 +#: assets/templates/assets/system_user_detail.html:157 +#: assets/templates/assets/system_user_detail.html:343 +msgid "Clear auth" +msgstr "清除认证信息" + +#: assets/templates/assets/system_user_detail.html:160 +msgid "Clear" +msgstr "清除" + +#: assets/templates/assets/system_user_detail.html:187 msgid "Add to node" msgstr "添加到节点" +#: assets/templates/assets/system_user_detail.html:343 +msgid "success" +msgstr "成功" + #: assets/templates/assets/system_user_list.html:18 #: assets/views/system_user.py:45 msgid "Create system user" msgstr "创建系统用户" -#: assets/templates/assets/system_user_list.html:134 +#: assets/templates/assets/system_user_list.html:135 msgid "This will delete the selected System Users !!!" msgstr "删除选择系统用户" -#: assets/templates/assets/system_user_list.html:142 +#: assets/templates/assets/system_user_list.html:143 msgid "System Users Deleted." msgstr "已被删除" -#: assets/templates/assets/system_user_list.html:143 -#: assets/templates/assets/system_user_list.html:148 +#: assets/templates/assets/system_user_list.html:144 +#: assets/templates/assets/system_user_list.html:149 msgid "System Users Delete" msgstr "删除系统用户" -#: assets/templates/assets/system_user_list.html:147 +#: assets/templates/assets/system_user_list.html:148 msgid "System Users Deleting failed." msgstr "系统用户删除失败" @@ -1123,7 +1169,7 @@ msgstr "批量更新资产" msgid "Update asset" msgstr "更新资产" -#: assets/views/asset.py:311 +#: assets/views/asset.py:314 msgid "already exists" msgstr "已经存在" @@ -1143,7 +1189,7 @@ msgstr "网域详情" msgid "Domain gateway list" msgstr "域网关列表" -#: assets/views/domain.py:151 +#: assets/views/domain.py:146 msgid "Update gateway" msgstr "创建网关" @@ -1176,7 +1222,7 @@ msgid "System user asset" msgstr "系统用户集群资产" #: audits/models.py:10 audits/templates/audits/ftp_log_list.html:74 -#: terminal/models.py:126 terminal/templates/terminal/session_list.html:74 +#: terminal/models.py:130 terminal/templates/terminal/session_list.html:74 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" msgstr "远端地址" @@ -1191,16 +1237,16 @@ msgid "Filename" msgstr "文件名" #: audits/models.py:15 audits/templates/audits/ftp_log_list.html:77 -#: ops/templates/ops/task_list.html:39 +#: ops/templates/ops/task_list.html:39 users/models/authentication.py:66 msgid "Success" msgstr "成功" #: audits/templates/audits/ftp_log_list.html:78 #: ops/templates/ops/adhoc_history.html:52 #: ops/templates/ops/adhoc_history_detail.html:61 -#: ops/templates/ops/task_history.html:58 perms/models.py:26 -#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:132 -#: terminal/templates/terminal/session_list.html:77 +#: ops/templates/ops/task_history.html:58 perms/models.py:36 +#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:137 +#: terminal/templates/terminal/session_list.html:78 msgid "Date start" msgstr "开始日期" @@ -1216,11 +1262,11 @@ msgstr "FTP日志" msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: common/api.py:52 +#: common/api.py:42 msgid "Test ldap success" msgstr "连接LDAP成功" -#: common/api.py:90 +#: common/api.py:80 msgid "Match {} s users" msgstr "匹配 {} 个用户" @@ -1355,7 +1401,7 @@ msgstr "密码认证" msgid "Public key auth" msgstr "密钥认证" -#: common/forms.py:159 common/templates/common/terminal_setting.html:63 +#: common/forms.py:159 common/templates/common/terminal_setting.html:68 #: terminal/forms.py:30 terminal/models.py:20 msgid "Command storage" msgstr "命令存储" @@ -1366,7 +1412,7 @@ msgid "" "other storage and some terminal using" msgstr "设置终端命令存储,default是默认用的存储方式" -#: common/forms.py:165 common/templates/common/terminal_setting.html:81 +#: common/forms.py:165 common/templates/common/terminal_setting.html:86 #: terminal/forms.py:35 terminal/models.py:21 msgid "Replay storage" msgstr "录像存储" @@ -1377,6 +1423,75 @@ msgid "" "other storage and some terminal using" msgstr "设置终端录像存储,default是默认用的存储方式" +#: common/forms.py:176 +msgid "MFA Secondary certification" +msgstr "MFA 二次认证" + +#: common/forms.py:178 +msgid "" +"After opening, the user login must use MFA secondary authentication (valid " +"for all users, including administrators)" +msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" + +#: common/forms.py:185 +msgid "Limit the number of login failures" +msgstr "限制登录失败次数" + +#: common/forms.py:190 +msgid "No logon interval" +msgstr "禁止登录时间间隔" + +#: common/forms.py:192 +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 "" +"提示:(单位 / 分钟)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录." + +#: common/forms.py:198 +msgid "Password minimum length" +msgstr "密码最小长度 " + +#: common/forms.py:205 +msgid "Must contain capital letters" +msgstr "必须包含大写字母" + +#: common/forms.py:207 +msgid "" +"After opening, the user password changes and resets must contain uppercase " +"letters" +msgstr "开启后,用户密码修改、重置必须包含大写字母" + +#: common/forms.py:213 +msgid "Must contain lowercase letters" +msgstr "必须包含小写字母" + +#: common/forms.py:214 +msgid "" +"After opening, the user password changes and resets must contain lowercase " +"letters" +msgstr "开启后,用户密码修改、重置必须包含小写字母" + +#: common/forms.py:220 +msgid "Must contain numeric characters" +msgstr "必须包含数字字符" + +#: common/forms.py:221 +msgid "" +"After opening, the user password changes and resets must contain numeric " +"characters" +msgstr "开启后,用户密码修改、重置必须包含数字字符" + +#: common/forms.py:227 +msgid "Must contain special characters" +msgstr "必须包含特殊字符" + +#: common/forms.py:228 +msgid "" +"After opening, the user password changes and resets must contain special " +"characters" +msgstr "开启后,用户密码修改、重置必须包含特殊字符" + #: common/mixins.py:29 msgid "is discard" msgstr "" @@ -1385,21 +1500,24 @@ msgstr "" msgid "discard time" msgstr "" -#: common/models.py:29 users/templates/users/user_detail.html:96 +#: common/models.py:29 users/models/authentication.py:51 +#: users/templates/users/user_detail.html:96 msgid "Enabled" msgstr "启用" #: common/templates/common/basic_setting.html:15 #: common/templates/common/email_setting.html:15 #: common/templates/common/ldap_setting.html:15 +#: common/templates/common/security_setting.html:15 #: common/templates/common/terminal_setting.html:16 -#: common/templates/common/terminal_setting.html:42 common/views.py:22 +#: common/templates/common/terminal_setting.html:46 common/views.py:22 msgid "Basic setting" msgstr "基本设置" #: common/templates/common/basic_setting.html:18 #: common/templates/common/email_setting.html:18 #: common/templates/common/ldap_setting.html:18 +#: common/templates/common/security_setting.html:18 #: common/templates/common/terminal_setting.html:20 common/views.py:48 msgid "Email setting" msgstr "邮件设置" @@ -1407,6 +1525,7 @@ msgstr "邮件设置" #: common/templates/common/basic_setting.html:21 #: common/templates/common/email_setting.html:21 #: common/templates/common/ldap_setting.html:21 +#: common/templates/common/security_setting.html:21 #: common/templates/common/terminal_setting.html:24 common/views.py:74 msgid "LDAP setting" msgstr "LDAP设置" @@ -1414,22 +1533,44 @@ msgstr "LDAP设置" #: common/templates/common/basic_setting.html:24 #: common/templates/common/email_setting.html:24 #: common/templates/common/ldap_setting.html:24 +#: common/templates/common/security_setting.html:24 #: common/templates/common/terminal_setting.html:28 common/views.py:104 msgid "Terminal setting" msgstr "终端设置" -#: common/templates/common/terminal_setting.html:68 -#: common/templates/common/terminal_setting.html:86 +#: common/templates/common/basic_setting.html:27 +#: common/templates/common/email_setting.html:27 +#: common/templates/common/ldap_setting.html:27 +#: common/templates/common/security_setting.html:27 +#: common/templates/common/terminal_setting.html:31 common/views.py:132 +msgid "Security setting" +msgstr "安全设置" + +#: common/templates/common/security_setting.html:42 +msgid "User login settings" +msgstr "用户登录设置" + +#: common/templates/common/security_setting.html:46 +msgid "Password check rule" +msgstr "密码校验规则" + +#: common/templates/common/terminal_setting.html:73 +#: common/templates/common/terminal_setting.html:91 #: users/templates/users/login_log_list.html:50 msgid "Type" msgstr "类型" +#: common/validators.py:7 +msgid "Special char not allowed" +msgstr "不能包含特殊字符" + #: common/views.py:21 common/views.py:47 common/views.py:73 common/views.py:103 -#: templates/_nav.html:81 +#: common/views.py:131 templates/_nav.html:81 msgid "Settings" msgstr "系统设置" #: common/views.py:32 common/views.py:58 common/views.py:86 common/views.py:116 +#: common/views.py:142 msgid "Update setting successfully, please restart program" msgstr "更新设置成功, 请手动重启程序" @@ -1678,7 +1819,7 @@ msgid "Versions" msgstr "版本" #: ops/templates/ops/task_list.html:40 -#: users/templates/users/login_log_list.html:54 +#: users/templates/users/login_log_list.html:57 msgid "Date" msgstr "日期" @@ -1703,17 +1844,17 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: perms/forms.py:18 users/forms.py:230 users/forms.py:235 users/forms.py:247 -#: users/forms.py:277 +#: perms/forms.py:18 users/forms.py:239 users/forms.py:244 users/forms.py:256 +#: users/forms.py:286 msgid "Select users" msgstr "选择用户" -#: perms/forms.py:34 perms/models.py:21 perms/models.py:68 +#: perms/forms.py:34 perms/models.py:31 perms/models.py:77 #: perms/templates/perms/asset_permission_list.html:55 #: perms/templates/perms/asset_permission_list.html:136 templates/_nav.html:14 -#: users/models/group.py:25 users/models/user.py:48 +#: users/models/group.py:23 users/models/user.py:55 #: users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:188 +#: users/templates/users/user_detail.html:192 #: users/templates/users/user_list.html:26 msgid "User group" msgstr "用户组" @@ -1726,14 +1867,14 @@ msgstr "" msgid "Asset or group at least one required" msgstr "" -#: perms/models.py:27 perms/models.py:71 +#: perms/models.py:37 perms/models.py:80 #: perms/templates/perms/asset_permission_detail.html:90 -#: users/models/user.py:80 users/templates/users/user_detail.html:103 -#: users/templates/users/user_profile.html:105 +#: users/models/user.py:87 users/templates/users/user_detail.html:107 +#: users/templates/users/user_profile.html:112 msgid "Date expired" msgstr "失效日期" -#: perms/models.py:81 templates/_nav.html:34 +#: perms/models.py:90 templates/_nav.html:34 msgid "Asset permission" msgstr "资产授权" @@ -1766,10 +1907,14 @@ msgid "Add node to this permission" msgstr "添加节点" #: perms/templates/perms/asset_permission_asset.html:125 -#: users/templates/users/user_detail.html:205 +#: users/templates/users/user_detail.html:209 msgid "Join" msgstr "加入" +#: perms/templates/perms/asset_permission_create_update.html:53 +msgid "Validity period" +msgstr "有效期" + #: perms/templates/perms/asset_permission_detail.html:66 msgid "User count" msgstr "用户数量" @@ -1852,14 +1997,14 @@ msgstr "商业支持" msgid "Docs" msgstr "文档" -#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:113 +#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:122 #: users/templates/users/_user.html:39 #: users/templates/users/first_login.html:39 -#: users/templates/users/user_password_update.html:37 +#: users/templates/users/user_password_update.html:39 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:322 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:343 msgid "Profile" msgstr "个人信息" @@ -1876,7 +2021,7 @@ msgid "Logout" msgstr "注销登录" #: templates/_header_bar.html:49 users/templates/users/login.html:44 -#: users/templates/users/login.html:64 +#: users/templates/users/login.html:68 msgid "Login" msgstr "登录" @@ -1916,13 +2061,13 @@ msgstr "关闭" #: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44 #: users/views/group.py:62 users/views/group.py:79 users/views/group.py:95 -#: users/views/login.py:241 users/views/login.py:289 users/views/user.py:64 -#: users/views/user.py:79 users/views/user.py:99 users/views/user.py:155 -#: users/views/user.py:310 users/views/user.py:357 users/views/user.py:379 +#: users/views/login.py:330 users/views/login.py:388 users/views/user.py:65 +#: users/views/user.py:80 users/views/user.py:102 users/views/user.py:175 +#: users/views/user.py:330 users/views/user.py:380 users/views/user.py:415 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:13 users/views/user.py:65 +#: templates/_nav.html:13 users/views/user.py:66 msgid "User list" msgstr "用户列表" @@ -1950,11 +2095,10 @@ msgstr "命令记录" msgid "Web terminal" msgstr "Web终端" -#: templates/_nav.html:51 terminal/templates/terminal/session_list.html:75 -#: terminal/views/command.py:47 terminal/views/session.py:75 -#: terminal/views/session.py:93 terminal/views/session.py:115 -#: terminal/views/terminal.py:31 terminal/views/terminal.py:46 -#: terminal/views/terminal.py:58 +#: templates/_nav.html:51 terminal/views/command.py:47 +#: terminal/views/session.py:75 terminal/views/session.py:93 +#: terminal/views/session.py:115 terminal/views/terminal.py:31 +#: terminal/views/terminal.py:46 terminal/views/terminal.py:58 msgid "Terminal" msgstr "终端管理" @@ -2033,26 +2177,26 @@ msgstr "线程数" msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:128 terminal/templates/terminal/session_list.html:102 +#: terminal/models.py:132 terminal/templates/terminal/session_list.html:104 msgid "Replay" msgstr "回放" -#: terminal/models.py:129 terminal/templates/terminal/command_list.html:55 +#: terminal/models.py:133 terminal/templates/terminal/command_list.html:55 #: terminal/templates/terminal/command_list.html:71 #: terminal/templates/terminal/session_detail.html:48 -#: terminal/templates/terminal/session_list.html:76 +#: terminal/templates/terminal/session_list.html:77 msgid "Command" msgstr "命令" -#: terminal/models.py:131 +#: terminal/models.py:136 msgid "Date last active" msgstr "最后活跃日期" -#: terminal/models.py:133 +#: terminal/models.py:138 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:150 +#: terminal/models.py:155 msgid "Args" msgstr "参数" @@ -2091,23 +2235,28 @@ msgstr "监控" msgid "Terminate session" msgstr "终止会话" -#: terminal/templates/terminal/session_list.html:79 +#: terminal/templates/terminal/session_list.html:76 +msgid "Login from" +msgstr "登录来源" + +#: terminal/templates/terminal/session_list.html:80 msgid "Duration" msgstr "时长" -#: terminal/templates/terminal/session_list.html:104 +#: terminal/templates/terminal/session_list.html:106 msgid "Monitor" msgstr "监控" -#: terminal/templates/terminal/session_list.html:105 +#: terminal/templates/terminal/session_list.html:108 +#: terminal/templates/terminal/session_list.html:110 msgid "Terminate" msgstr "终断" -#: terminal/templates/terminal/session_list.html:116 +#: terminal/templates/terminal/session_list.html:122 msgid "Terminate selected" msgstr "终断所选" -#: terminal/templates/terminal/session_list.html:136 +#: terminal/templates/terminal/session_list.html:142 msgid "Terminate task send, waiting ..." msgstr "终断任务已发送,请等待" @@ -2177,6 +2326,10 @@ msgid "" "You should use your ssh client tools connect terminal: {}

    {}" msgstr "你可以使用ssh客户端工具连接终端" +#: users/api.py:208 users/templates/users/login.html:50 +msgid "Log in frequently and try again later" +msgstr "登录频繁, 稍后重试" + #: users/authentication.py:56 msgid "Invalid signature header. No credentials provided." msgstr "" @@ -2228,11 +2381,11 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: users/forms.py:30 +#: users/forms.py:39 msgid "MFA code" msgstr "MFA 验证码" -#: users/forms.py:41 users/models/user.py:52 +#: users/forms.py:50 users/models/user.py:59 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 @@ -2240,31 +2393,31 @@ msgstr "MFA 验证码" msgid "Role" msgstr "角色" -#: users/forms.py:44 users/forms.py:193 +#: users/forms.py:53 users/forms.py:202 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:45 users/forms.py:194 +#: users/forms.py:54 users/forms.py:203 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:46 +#: users/forms.py:55 msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:64 users/templates/users/user_detail.html:196 +#: users/forms.py:73 users/templates/users/user_detail.html:200 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:75 users/forms.py:208 +#: users/forms.py:84 users/forms.py:217 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:79 users/forms.py:212 users/serializers.py:45 +#: users/forms.py:88 users/forms.py:221 users/serializers.py:48 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:119 +#: users/forms.py:128 msgid "" "Tip: when enabled, you will enter the MFA binding process the next time you " "log in. you can also directly bind in \"personal information -> quick " @@ -2273,16 +2426,17 @@ msgstr "" "提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修" "改->更改MFA设置)中直接绑定!" -#: users/forms.py:129 +#: users/forms.py:138 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全." -#: users/forms.py:134 users/models/user.py:64 +#: users/forms.py:143 users/models/authentication.py:75 users/models/user.py:71 #: users/templates/users/first_login.html:45 +#: users/templates/users/login_log_list.html:54 msgid "MFA" msgstr "MFA" -#: users/forms.py:139 +#: users/forms.py:148 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -2291,42 +2445,43 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:146 users/templates/users/first_login.html:48 -#: users/templates/users/first_login.html:110 +#: users/forms.py:155 users/templates/users/first_login.html:48 +#: users/templates/users/first_login.html:107 +#: users/templates/users/first_login.html:130 msgid "Finish" msgstr "完成" -#: users/forms.py:152 +#: users/forms.py:161 msgid "Old password" msgstr "原来密码" -#: users/forms.py:157 +#: users/forms.py:166 msgid "New password" msgstr "新密码" -#: users/forms.py:162 +#: users/forms.py:171 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:172 +#: users/forms.py:181 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:180 +#: users/forms.py:189 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:191 +#: users/forms.py:200 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:195 +#: users/forms.py:204 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:223 users/models/user.py:72 +#: users/forms.py:232 users/models/user.py:79 #: users/templates/users/first_login.html:42 -#: users/templates/users/user_password_update.html:43 +#: users/templates/users/user_password_update.html:45 #: users/templates/users/user_profile.html:68 #: users/templates/users/user_profile_update.html:43 #: users/templates/users/user_pubkey_update.html:43 @@ -2337,63 +2492,99 @@ msgstr "ssh公钥" msgid "Private Token" msgstr "ssh密钥" -#: users/models/authentication.py:46 +#: users/models/authentication.py:50 users/templates/users/user_detail.html:98 +msgid "Disabled" +msgstr "禁用" + +#: users/models/authentication.py:52 users/models/authentication.py:60 +msgid "-" +msgstr "" + +#: users/models/authentication.py:61 +msgid "Username/password check failed" +msgstr "用户名/密码 校验失败" + +#: users/models/authentication.py:62 +msgid "MFA authentication failed" +msgstr "MFA 认证失败" + +#: users/models/authentication.py:67 +msgid "Failed" +msgstr "失败" + +#: users/models/authentication.py:71 msgid "Login type" msgstr "登录方式" -#: users/models/authentication.py:47 +#: users/models/authentication.py:72 msgid "Login ip" msgstr "登录IP" -#: users/models/authentication.py:48 +#: users/models/authentication.py:73 msgid "Login city" msgstr "登录城市" -#: users/models/authentication.py:49 +#: users/models/authentication.py:74 msgid "User agent" msgstr "Agent" -#: users/models/authentication.py:50 +#: users/models/authentication.py:76 +#: users/templates/users/login_log_list.html:55 +msgid "Reason" +msgstr "原因" + +#: users/models/authentication.py:77 +#: users/templates/users/login_log_list.html:56 +msgid "Status" +msgstr "状态" + +#: users/models/authentication.py:78 msgid "Date login" msgstr "登录日期" -#: users/models/user.py:29 users/models/user.py:326 +#: users/models/user.py:30 users/models/user.py:341 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:31 +#: users/models/user.py:32 msgid "Application" msgstr "应用程序" -#: users/models/user.py:34 users/templates/users/user_profile.html:92 -#: users/templates/users/user_profile.html:156 -#: users/templates/users/user_profile.html:159 +#: users/models/user.py:35 users/templates/users/user_profile.html:92 +#: users/templates/users/user_profile.html:163 +#: users/templates/users/user_profile.html:166 msgid "Disable" msgstr "禁用" -#: users/models/user.py:35 users/templates/users/user_profile.html:90 -#: users/templates/users/user_profile.html:163 +#: users/models/user.py:36 users/templates/users/user_profile.html:90 +#: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" -#: users/models/user.py:36 users/templates/users/user_profile.html:88 +#: users/models/user.py:37 users/templates/users/user_profile.html:88 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:44 users/templates/users/user_detail.html:71 +#: users/models/user.py:51 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" -#: users/models/user.py:55 +#: users/models/user.py:62 msgid "Avatar" msgstr "头像" -#: users/models/user.py:58 users/templates/users/user_detail.html:82 +#: users/models/user.py:65 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:329 +#: users/models/user.py:94 users/templates/users/user_detail.html:103 +#: users/templates/users/user_list.html:27 +#: users/templates/users/user_profile.html:100 +msgid "Source" +msgstr "用户来源" + +#: users/models/user.py:344 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -2443,11 +2634,15 @@ msgstr "首次登陆" msgid "I agree with the terms and conditions." msgstr "我同意条款和条件" -#: users/templates/users/first_login.html:100 +#: users/templates/users/first_login.html:73 +msgid "Please choose the terms and conditions." +msgstr "请选择同意条款和条件" + +#: users/templates/users/first_login.html:101 msgid "Previous" msgstr "上一步" -#: users/templates/users/first_login.html:108 +#: users/templates/users/first_login.html:105 #: users/templates/users/login_otp.html:66 #: users/templates/users/user_otp_authentication.html:22 #: users/templates/users/user_otp_enable_bind.html:19 @@ -2469,7 +2664,7 @@ msgid " for more information" msgstr "获取更多信息" #: users/templates/users/forgot_password.html:26 -#: users/templates/users/login.html:73 +#: users/templates/users/login.html:77 msgid "Forgot password" msgstr "忘记密码" @@ -2477,7 +2672,7 @@ msgstr "忘记密码" msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" -#: users/templates/users/login.html:50 +#: users/templates/users/login.html:53 msgid "Captcha invalid" msgstr "验证码错误" @@ -2505,22 +2700,34 @@ msgstr "6位数字" msgid "Can't provide security? Please contact the administrator!" msgstr "如果不能提供MFA验证码,请联系管理员!" -#: users/templates/users/reset_password.html:45 -#: users/templates/users/user_detail.html:348 users/utils.py:73 +#: users/templates/users/reset_password.html:46 +#: users/templates/users/user_detail.html:352 users/utils.py:80 msgid "Reset password" msgstr "重置密码" -#: users/templates/users/reset_password.html:55 +#: users/templates/users/reset_password.html:59 +#: users/templates/users/user_password_update.html:60 +#: users/templates/users/user_update.html:12 +msgid "Your password must satisfy" +msgstr "您的密码必须满足:" + +#: users/templates/users/reset_password.html:60 +#: users/templates/users/user_password_update.html:61 +#: users/templates/users/user_update.html:13 +msgid "Password strength" +msgstr "密码强度:" + +#: users/templates/users/reset_password.html:66 msgid "Password again" msgstr "再次输入密码" -#: users/templates/users/reset_password.html:57 +#: users/templates/users/reset_password.html:68 #: users/templates/users/user_profile.html:20 msgid "Setting" msgstr "设置" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:16 users/views/user.py:79 +#: users/templates/users/user_list.html:16 users/views/user.py:80 msgid "Create user" msgstr "创建用户" @@ -2529,7 +2736,7 @@ msgid "Reset link will be generated and sent to the user. " msgstr "生成重置密码连接,通过邮件发送给用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:156 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:176 msgid "User detail" msgstr "用户详情" @@ -2544,67 +2751,63 @@ msgstr "授权的资产" msgid "Force enabled" msgstr "强制启用" -#: users/templates/users/user_detail.html:98 -msgid "Disabled" -msgstr "禁用" - -#: users/templates/users/user_detail.html:115 -#: users/templates/users/user_profile.html:101 +#: users/templates/users/user_detail.html:119 +#: users/templates/users/user_profile.html:108 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:151 +#: users/templates/users/user_detail.html:155 msgid "Force enabled MFA" msgstr "强制启用MFA" -#: users/templates/users/user_detail.html:166 +#: users/templates/users/user_detail.html:170 msgid "Send reset password mail" msgstr "发送重置密码邮件" -#: users/templates/users/user_detail.html:169 -#: users/templates/users/user_detail.html:177 +#: users/templates/users/user_detail.html:173 +#: users/templates/users/user_detail.html:181 msgid "Send" msgstr "发送" -#: users/templates/users/user_detail.html:174 +#: users/templates/users/user_detail.html:178 msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" -#: users/templates/users/user_detail.html:291 +#: users/templates/users/user_detail.html:295 msgid "Goto profile page enable MFA" msgstr "请去个人信息页面启用自己的MFA" -#: users/templates/users/user_detail.html:347 +#: users/templates/users/user_detail.html:351 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:358 +#: users/templates/users/user_detail.html:362 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:372 +#: users/templates/users/user_detail.html:376 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:373 +#: users/templates/users/user_detail.html:377 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:383 +#: users/templates/users/user_detail.html:387 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:400 -#: users/templates/users/user_profile.html:204 +#: users/templates/users/user_detail.html:404 +#: users/templates/users/user_profile.html:211 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:401 #: users/templates/users/user_detail.html:405 -#: users/templates/users/user_profile.html:205 -#: users/templates/users/user_profile.html:210 +#: users/templates/users/user_detail.html:409 +#: users/templates/users/user_profile.html:212 +#: users/templates/users/user_profile.html:217 msgid "User SSH public key update" msgstr "ssh密钥" @@ -2643,45 +2846,49 @@ msgstr "用户组删除" msgid "UserGroup Deleting failed." msgstr "用户组删除失败" -#: users/templates/users/user_list.html:192 +#: users/templates/users/user_list.html:196 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" -#: users/templates/users/user_list.html:200 +#: users/templates/users/user_list.html:204 msgid "User Deleted." msgstr "已被删除" -#: users/templates/users/user_list.html:201 -#: users/templates/users/user_list.html:206 +#: users/templates/users/user_list.html:205 +#: users/templates/users/user_list.html:210 msgid "User Delete" msgstr "删除" -#: users/templates/users/user_list.html:205 +#: users/templates/users/user_list.html:209 msgid "User Deleting failed." msgstr "用户删除失败" -#: users/templates/users/user_profile.html:109 users/views/user.py:185 -#: users/views/user.py:239 +#: users/templates/users/user_profile.html:95 +msgid "Administrator Settings force MFA login" +msgstr "管理员设置强制使用MFA登录" + +#: users/templates/users/user_profile.html:116 users/views/user.py:205 +#: users/views/user.py:259 msgid "User groups" msgstr "用户组" -#: users/templates/users/user_profile.html:141 +#: users/templates/users/user_profile.html:148 msgid "Update password" msgstr "更改密码" -#: users/templates/users/user_profile.html:149 +#: users/templates/users/user_profile.html:156 msgid "Update MFA settings" msgstr "更改MFA设置" -#: users/templates/users/user_profile.html:170 +#: users/templates/users/user_profile.html:177 msgid "Update SSH public key" msgstr "更改SSH密钥" -#: users/templates/users/user_profile.html:178 +#: users/templates/users/user_profile.html:185 msgid "Reset public key and download" msgstr "重置并下载SSH密钥" -#: users/templates/users/user_profile.html:208 +#: users/templates/users/user_profile.html:215 msgid "Failed to update SSH public key." msgstr "更新密钥失败" @@ -2701,15 +2908,21 @@ msgstr "更新密钥" msgid "Or reset by server" msgstr "或者重置并下载密钥" -#: users/templates/users/user_update.html:4 users/views/user.py:99 +#: users/templates/users/user_pubkey_update.html:94 +msgid "" +"The new public key has been set successfully, Please download the " +"corresponding private key." +msgstr "新的公钥已设置成功,请下载对应的私钥" + +#: users/templates/users/user_update.html:4 users/views/user.py:103 msgid "Update user" msgstr "更新用户" -#: users/utils.py:37 +#: users/utils.py:41 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:39 +#: users/utils.py:43 #, python-format msgid "" "\n" @@ -2717,6 +2930,8 @@ msgid "" "
    \n" " Your account has been created successfully\n" "
    \n" +" Username: %(username)s\n" +"
    \n" " click " "here to set your password\n" "
    \n" @@ -2736,6 +2951,8 @@ msgstr "" " 你好 %(name)s:\n" "
    \n" " 恭喜您,您的账号已经创建成功
    \n" +" 用户名: %(username)s\n" +"
    \n" " 请点击这" "里设置密码
    \n" " 这个链接有效期1小时, 超过时间您可以 \n" " " -#: users/utils.py:75 +#: users/utils.py:82 #, python-format msgid "" "\n" @@ -2794,11 +3011,11 @@ msgstr "" "
    \n" " " -#: users/utils.py:106 +#: users/utils.py:113 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:108 +#: users/utils.py:115 #, python-format msgid "" "\n" @@ -2823,18 +3040,22 @@ msgstr "" "
    \n" " " -#: users/utils.py:141 +#: users/utils.py:148 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:143 +#: users/utils.py:150 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:156 +#: users/utils.py:163 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" +#: users/utils.py:289 users/utils.py:299 +msgid "Bit" +msgstr " 位" + #: users/views/group.py:29 msgid "User group list" msgstr "用户组列表" @@ -2847,105 +3068,106 @@ msgstr "更新用户组" msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:56 +#: users/views/login.py:75 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:107 users/views/user.py:464 users/views/user.py:489 +#: users/views/login.py:178 users/views/user.py:500 users/views/user.py:525 msgid "MFA code invalid" msgstr "MFA码认证失败" -#: users/views/login.py:133 +#: users/views/login.py:207 msgid "Logout success" msgstr "退出登录成功" -#: users/views/login.py:134 +#: users/views/login.py:208 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: users/views/login.py:150 +#: users/views/login.py:224 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:163 +#: users/views/login.py:237 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:164 +#: users/views/login.py:238 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:177 +#: users/views/login.py:251 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:178 +#: users/views/login.py:252 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:195 users/views/login.py:208 +#: users/views/login.py:273 users/views/login.py:286 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:204 +#: users/views/login.py:282 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:241 +#: users/views/login.py:292 users/views/user.py:118 users/views/user.py:398 +msgid "* Your password does not meet the requirements" +msgstr "* 您的密码不符合要求" + +#: users/views/login.py:330 msgid "First login" msgstr "首次登陆" -#: users/views/login.py:290 +#: users/views/login.py:389 msgid "Login log list" msgstr "登录日志" -#: users/views/user.py:109 +#: users/views/user.py:129 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:214 +#: users/views/user.py:234 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:311 +#: users/views/user.py:331 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:340 +#: users/views/user.py:362 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:358 +#: users/views/user.py:381 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:380 +#: users/views/user.py:416 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:421 +#: users/views/user.py:457 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:515 +#: users/views/user.py:551 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:516 +#: users/views/user.py:552 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:518 +#: users/views/user.py:554 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:519 +#: users/views/user.py:555 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" -#~ msgid "Step" -#~ msgstr "Step" - -#~ msgid "Add asset" -#~ msgstr "添加资产到节点" +#~ msgid "MFA setting" +#~ msgstr "MFA 设置" diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 0afcd4e72..c0a536276 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -229,7 +229,11 @@ LOGGING = { 'django_auth_ldap': { 'handlers': ['console', 'ansible_logs'], 'level': "INFO", - } + }, + # 'django.db': { + # 'handlers': ['console', 'file'], + # 'level': 'DEBUG' + # } } } @@ -329,6 +333,9 @@ AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER AUTH_LDAP_GROUP_SEARCH = LDAPSearch( AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER ) +AUTH_LDAP_CONNECTION_OPTIONS = { + ldap.OPT_TIMEOUT: 5 +} AUTH_LDAP_ALWAYS_UPDATE_USER = True AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' @@ -336,10 +343,11 @@ if AUTH_LDAP: AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) # Celery using redis as broker -CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/3' % { +CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'host': CONFIG.REDIS_HOST or '127.0.0.1', 'port': CONFIG.REDIS_PORT or 6379, + 'db':CONFIG.REDIS_DB_CELERY_BROKER or 3, } CELERY_TASK_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle' @@ -360,10 +368,11 @@ CELERY_WORKER_HIJACK_ROOT_LOGGER = False CACHES = { 'default': { 'BACKEND': 'redis_cache.RedisCache', - 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/4' % { + 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'host': CONFIG.REDIS_HOST or '127.0.0.1', 'port': CONFIG.REDIS_PORT or 6379, + 'db':CONFIG.REDIS_DB_CACHE or 4, } } } @@ -394,6 +403,11 @@ TERMINAL_REPLAY_STORAGE = { }, } + +DEFAULT_PASSWORD_MIN_LENGTH = 6 +DEFAULT_LOGIN_LIMIT_COUNT = 3 +DEFAULT_LOGIN_LIMIT_TIME = 30 + # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { 'horizontal_label_class': 'col-md-2', diff --git a/apps/ops/api.py b/apps/ops/api.py index 0134fbd4a..35c9b8dc5 100644 --- a/apps/ops/api.py +++ b/apps/ops/api.py @@ -72,7 +72,7 @@ class CeleryTaskLogApi(generics.RetrieveAPIView): def get(self, request, *args, **kwargs): mark = request.query_params.get("mark") or str(uuid.uuid4()) - task = super().get_object() + task = self.get_object() log_path = task.full_log_path if not log_path or not os.path.isfile(log_path): diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index 6230e8167..7ed89e75c 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -86,13 +86,14 @@ class JMSInventory(BaseInventory): gateway = asset.domain.random_gateway() proxy_command_list = [ "ssh", "-p", str(gateway.port), + "-o", "StrictHostKeyChecking=no", "{}@{}".format(gateway.username, gateway.ip), "-W", "%h:%p", "-q", ] if gateway.password: proxy_command_list.insert( - 0, "sshpass -p {}".format(gateway.password) + 0, "sshpass -p '{}'".format(gateway.password) ) if gateway.private_key: proxy_command_list.append("-i {}".format(gateway.private_key_file)) diff --git a/apps/ops/templates/ops/adhoc_history_detail.html b/apps/ops/templates/ops/adhoc_history_detail.html index 16adbc4e3..b8a48d4e5 100644 --- a/apps/ops/templates/ops/adhoc_history_detail.html +++ b/apps/ops/templates/ops/adhoc_history_detail.html @@ -19,7 +19,7 @@
    {% trans 'Run history detail' %}
  • - {% trans 'Output' %} + {% trans 'Output' %}
  • diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html index 9b0826949..13885c2cc 100644 --- a/apps/ops/templates/ops/celery_task_log.html +++ b/apps/ops/templates/ops/celery_task_log.html @@ -2,38 +2,25 @@ term.js + + -
    -
    +
    -
    - - + {% block custom_head_css_js_create %} {% endblock %} {% endblock %} diff --git a/apps/templates/_footer.html b/apps/templates/_footer.html index f23c89399..c78f0da33 100644 --- a/apps/templates/_footer.html +++ b/apps/templates/_footer.html @@ -1,7 +1,7 @@
    diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index 63a2c9209..33ae09877 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -73,6 +73,7 @@ + {# #} @@ -92,6 +93,7 @@ + @@ -99,10 +101,14 @@ diff --git a/apps/terminal/templatetags/terminal_tags.py b/apps/terminal/templatetags/terminal_tags.py index cd7120fec..c5643c67b 100644 --- a/apps/terminal/templatetags/terminal_tags.py +++ b/apps/terminal/templatetags/terminal_tags.py @@ -1,10 +1,10 @@ # ~*~ coding: utf-8 ~*~ from django import template -from ..backends import get_multi_command_store +from ..backends import get_multi_command_storage register = template.Library() -command_store = get_multi_command_store() +command_store = get_multi_command_storage() @register.filter diff --git a/apps/terminal/views/command.py b/apps/terminal/views/command.py index 0af0b5bfd..748261414 100644 --- a/apps/terminal/views/command.py +++ b/apps/terminal/views/command.py @@ -9,10 +9,10 @@ from django.utils.translation import ugettext as _ from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from ..models import Command from .. import utils -from ..backends import get_multi_command_store +from ..backends import get_multi_command_storage __all__ = ['CommandListView'] -common_storage = get_multi_command_store() +common_storage = get_multi_command_storage() class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView): diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py index 3b66baff7..71caeae48 100644 --- a/apps/terminal/views/session.py +++ b/apps/terminal/views/session.py @@ -10,7 +10,7 @@ from django.conf import settings from users.utils import AdminUserRequiredMixin from common.mixins import DatetimeSearchMixin from ..models import Session, Command, Terminal -from ..backends import get_multi_command_store +from ..backends import get_multi_command_storage from .. import utils @@ -19,7 +19,7 @@ __all__ = [ 'SessionDetailView', ] -command_store = get_multi_command_store() +command_store = get_multi_command_storage() class SessionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): diff --git a/apps/users/api.py b/apps/users/api.py index dbc5b66a8..c23112384 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -3,6 +3,7 @@ import uuid from django.core.cache import cache from django.urls import reverse +from django.utils.translation import ugettext as _ from rest_framework import generics from rest_framework.permissions import AllowAny, IsAuthenticated @@ -14,10 +15,11 @@ from .serializers import UserSerializer, UserGroupSerializer, \ UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \ UserUpdateGroupSerializer, ChangeUserPasswordSerializer from .tasks import write_login_log_async -from .models import User, UserGroup +from .models import User, UserGroup, LoginLog from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \ IsSuperUserOrAppUser -from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code +from .utils import check_user_valid, generate_token, get_login_ip, \ + check_otp_code, set_user_login_failed_count_to_cache, is_block_login from common.mixins import IDInFilterMixin from common.utils import get_logger @@ -128,16 +130,12 @@ class UserToken(APIView): return Response({'error': msg}, status=406) -class UserProfile(APIView): - permission_classes = (IsValidUser,) +class UserProfile(generics.RetrieveAPIView): + permission_classes = (IsAuthenticated,) serializer_class = UserSerializer - def get(self, request): - # return Response(request.user.to_json()) - return Response(self.serializer_class(request.user).data) - - def post(self, request): - return Response(self.serializer_class(request.user).data) + def get_object(self): + return self.request.user class UserOtpAuthApi(APIView): @@ -153,10 +151,23 @@ class UserOtpAuthApi(APIView): return Response({'msg': '请先进行用户名和密码验证'}, status=401) if not check_otp_code(user.otp_secret_key, otp_code): + data = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_MFA, + 'status': False + } + self.write_login_log(request, data) return Response({'msg': 'MFA认证失败'}, status=401) + data = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_NOTHING, + 'status': True + } + self.write_login_log(request, data) token = generate_token(request, user) - self.write_login_log(request, user) return Response( { 'token': token, @@ -165,7 +176,7 @@ class UserOtpAuthApi(APIView): ) @staticmethod - def write_login_log(request, user): + def write_login_log(request, data): login_ip = request.data.get('remote_addr', None) login_type = request.data.get('login_type', '') user_agent = request.data.get('HTTP_USER_AGENT', '') @@ -173,25 +184,52 @@ class UserOtpAuthApi(APIView): if not login_ip: login_ip = get_login_ip(request) - write_login_log_async.delay( - user.username, ip=login_ip, - type=login_type, user_agent=user_agent, - ) + tmp_data = { + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent + } + data.update(tmp_data) + write_login_log_async.delay(**data) class UserAuthApi(APIView): permission_classes = (AllowAny,) serializer_class = UserSerializer + key_prefix_limit = "_LOGIN_LIMIT_{}_{}" def post(self, request): - user, msg = self.check_user_valid(request) + # limit login + username = request.data.get('username') + ip = request.data.get('remote_addr', None) + ip = ip if ip else get_login_ip(request) + key_limit = self.key_prefix_limit.format(ip, username) + if is_block_login(key_limit): + msg = _("Log in frequently and try again later") + return Response({'msg': msg}, status=401) + user, msg = self.check_user_valid(request) if not user: + data = { + 'username': request.data.get('username', ''), + 'mfa': LoginLog.MFA_UNKNOWN, + 'reason': LoginLog.REASON_PASSWORD, + 'status': False + } + self.write_login_log(request, data) + + set_user_login_failed_count_to_cache(key_limit) return Response({'msg': msg}, status=401) if not user.otp_enabled: + data = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_NOTHING, + 'status': True + } + self.write_login_log(request, data) token = generate_token(request, user) - self.write_login_log(request, user) return Response( { 'token': token, @@ -208,7 +246,8 @@ class UserAuthApi(APIView): 'otp_url': reverse('api-users:user-otp-auth'), 'seed': seed, 'user': self.serializer_class(user).data - }, status=300) + }, status=300 + ) @staticmethod def check_user_valid(request): @@ -222,7 +261,7 @@ class UserAuthApi(APIView): return user, msg @staticmethod - def write_login_log(request, user): + def write_login_log(request, data): login_ip = request.data.get('remote_addr', None) login_type = request.data.get('login_type', '') user_agent = request.data.get('HTTP_USER_AGENT', '') @@ -230,10 +269,14 @@ class UserAuthApi(APIView): if not login_ip: login_ip = get_login_ip(request) - write_login_log_async.delay( - user.username, ip=login_ip, - type=login_type, user_agent=user_agent, - ) + tmp_data = { + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent, + } + data.update(tmp_data) + + write_login_log_async.delay(**data) class UserConnectionTokenApi(APIView): diff --git a/apps/users/forms.py b/apps/users/forms.py index f777e0dd7..165694c3f 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -16,13 +16,14 @@ class UserLoginForm(AuthenticationForm): max_length=128, strip=False ) + def confirm_login_allowed(self, user): + if not user.is_staff: + raise forms.ValidationError( + self.error_messages['inactive'], + code='inactive',) -class UserLoginCaptchaForm(AuthenticationForm): - username = forms.CharField(label=_('Username'), max_length=100) - password = forms.CharField( - label=_('Password'), widget=forms.PasswordInput, - max_length=128, strip=False - ) + +class UserLoginCaptchaForm(UserLoginForm): captcha = CaptchaField() @@ -72,7 +73,7 @@ class UserCreateUpdateForm(forms.ModelForm): 'data-placeholder': _('Join user groups') } ), - 'otp_level': forms.RadioSelect() + 'otp_level': forms.RadioSelect(), } def clean_public_key(self): diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py index 5169a79d2..cb0b7d85f 100644 --- a/apps/users/models/authentication.py +++ b/apps/users/models/authentication.py @@ -41,12 +41,40 @@ class LoginLog(models.Model): ('W', 'Web'), ('T', 'Terminal'), ) + + MFA_DISABLED = 0 + MFA_ENABLED = 1 + MFA_UNKNOWN = 2 + + MFA_CHOICE = ( + (MFA_DISABLED, _('Disabled')), + (MFA_ENABLED, _('Enabled')), + (MFA_UNKNOWN, _('-')), + ) + + REASON_NOTHING = 0 + REASON_PASSWORD = 1 + REASON_MFA = 2 + + REASON_CHOICE = ( + (REASON_NOTHING, _('-')), + (REASON_PASSWORD, _('Username/password check failed')), + (REASON_MFA, _('MFA authentication failed')), + ) + + STATUS_CHOICE = ( + (True, _('Success')), + (False, _('Failed')) + ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) username = models.CharField(max_length=20, verbose_name=_('Username')) type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) ip = models.GenericIPAddressField(verbose_name=_('Login ip')) city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city')) user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent')) + mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA')) + reason = models.SmallIntegerField(default=REASON_NOTHING, choices=REASON_CHOICE, verbose_name=_('Reason')) + status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status')) datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login')) class Meta: diff --git a/apps/users/models/group.py b/apps/users/models/group.py index 128fbdbcb..9196156f7 100644 --- a/apps/users/models/group.py +++ b/apps/users/models/group.py @@ -4,14 +4,12 @@ import uuid from django.db import models, IntegrityError from django.utils.translation import ugettext_lazy as _ -from common.mixins import NoDeleteModelMixin - __all__ = ['UserGroup'] -class UserGroup(NoDeleteModelMixin): +class UserGroup(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) + name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 23c30cf77..d69151f55 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -14,6 +14,7 @@ from django.utils import timezone from django.shortcuts import reverse from common.utils import get_signer, date_expired_default +from common.models import Setting __all__ = ['User'] @@ -35,6 +36,12 @@ class User(AbstractUser): (1, _('Enable')), (2, _("Force enable")), ) + SOURCE_LOCAL = 'local' + SOURCE_LDAP = 'ldap' + SOURCE_CHOICES = ( + (SOURCE_LOCAL, 'Local'), + (SOURCE_LDAP, 'LDAP/AD'), + ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) username = models.CharField( max_length=128, unique=True, verbose_name=_('Username') @@ -77,11 +84,15 @@ class User(AbstractUser): is_first_login = models.BooleanField(default=True) date_expired = models.DateTimeField( default=date_expired_default, blank=True, null=True, - verbose_name=_('Date expired') + 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') + ) def __str__(self): return '{0.name}({0.username})'.format(self) @@ -248,14 +259,17 @@ class User(AbstractUser): @property def otp_enabled(self): - return self.otp_level > 0 + return self.otp_force_enabled or self.otp_level > 0 @property def otp_force_enabled(self): + mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first() + if mfa_setting and mfa_setting.cleaned_value: + return True return self.otp_level == 2 def enable_otp(self): - if not self.otp_force_enabled: + if not self.otp_level == 2: self.otp_level = 1 def force_enable_otp(self): @@ -275,6 +289,7 @@ class User(AbstractUser): '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, diff --git a/apps/users/serializers.py b/apps/users/serializers.py index f1347b0d5..d3f3eb73f 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -26,7 +26,10 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): def get_field_names(self, declared_fields, info): fields = super(UserSerializer, self).get_field_names(declared_fields, info) - fields.extend(['groups_display', 'get_role_display', 'is_valid']) + fields.extend([ + 'groups_display', 'get_role_display', + 'get_source_display', 'is_valid' + ]) return fields @staticmethod diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 4c6afc663..bae5e37cd 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -2,6 +2,7 @@ # from django.dispatch import receiver +from django_auth_ldap.backend import populate_user # from django.db.models.signals import post_save from common.utils import get_logger @@ -28,3 +29,11 @@ def on_user_create(sender, user=None, **kwargs): logger.info(" - Sending welcome mail ...".format(user.name)) if user.email: send_user_created_mail(user) + + +@receiver(populate_user) +def on_ldap_create_user(sender, user, ldap_user, **kwargs): + if user: + user.source = user.SOURCE_LDAP + user.save() + diff --git a/apps/users/templates/users/_user.html b/apps/users/templates/users/_user.html index f973f3344..5090e825f 100644 --- a/apps/users/templates/users/_user.html +++ b/apps/users/templates/users/_user.html @@ -48,6 +48,7 @@ + {% endblock %} {% block custom_foot_js %} diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html index 7138199a3..9687949d1 100644 --- a/apps/users/templates/users/first_login.html +++ b/apps/users/templates/users/first_login.html @@ -70,6 +70,7 @@
    + {% endif %} {% bootstrap_form wizard.form %} @@ -99,11 +100,7 @@ {% if wizard.steps.prev %}
  • {% trans "Previous" %}
  • {% endif %} - {#{% if wizard.steps.next %}#} - {#
  • {% trans "Next" %}
  • #} - {#{% endif %}#} - {#
  • {% trans "Submit" %}
  • #} - {#将原来的下一页-替换为提交;修复 每页都提交,最后才能成功问题#} + {% if wizard.steps.next %}
  • {% trans "Next" %}
  • {% else %} @@ -124,20 +121,26 @@ {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/login.html b/apps/users/templates/users/login.html index 7dd3f5d0a..6b55a58bf 100644 --- a/apps/users/templates/users/login.html +++ b/apps/users/templates/users/login.html @@ -45,13 +45,17 @@ {% csrf_token %} - {% if form.errors %} + + {% if block_login %} +

    {% trans 'Log in frequently and try again later' %}

    + {% elif form.errors %} {% if 'captcha' in form.errors %}

    {% trans 'Captcha invalid' %}

    {% else %}

    {{ form.non_field_errors.as_text }}

    {% endif %} {% endif %} +
    diff --git a/apps/users/templates/users/login_log_list.html b/apps/users/templates/users/login_log_list.html index 4a08c28db..afaf671a5 100644 --- a/apps/users/templates/users/login_log_list.html +++ b/apps/users/templates/users/login_log_list.html @@ -51,6 +51,9 @@ + + + {% endblock %} @@ -65,6 +68,9 @@ + + + {% endfor %} diff --git a/apps/users/templates/users/reset_password.html b/apps/users/templates/users/reset_password.html index 0bab32809..3d669a1e5 100644 --- a/apps/users/templates/users/reset_password.html +++ b/apps/users/templates/users/reset_password.html @@ -11,6 +11,7 @@ {% include '_head_css_js.html' %} + @@ -49,10 +50,20 @@

    {{ errors }}

    {% endif %}
    - + + {# 密码popover #} +
    + +
    - +
    @@ -79,4 +90,33 @@ + diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 66bfa61e0..d40251563 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -99,6 +99,10 @@ {% endif %} + + + + @@ -417,8 +421,8 @@ $(document).ready(function() { APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail}); }).on('click', '.btn-delete-user', function () { var $this = $(this); - var name = "{{ user.name }}"; - var uid = "{{ user.id }}"; + var name = "{{ user_object.name }}"; + var uid = "{{ user_object.id }}"; var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid); var redirect_url = "{% url 'users:user-list' %}"; objectDelete($this, name, the_url, redirect_url); diff --git a/apps/users/templates/users/user_granted_asset.html b/apps/users/templates/users/user_granted_asset.html index ec807aec5..299c33e07 100644 --- a/apps/users/templates/users/user_granted_asset.html +++ b/apps/users/templates/users/user_granted_asset.html @@ -68,7 +68,7 @@ var asset_table; function initTable() { if (inited){ - return + return asset_table } else { inited = true; } diff --git a/apps/users/templates/users/user_group_granted_asset.html b/apps/users/templates/users/user_group_granted_asset.html index c1d56a701..1692e06a2 100644 --- a/apps/users/templates/users/user_group_granted_asset.html +++ b/apps/users/templates/users/user_group_granted_asset.html @@ -64,10 +64,11 @@ var zTree; var inited = false; var url; +var asset_table; function initTable() { if (inited){ - return + return asset_table } else { inited = true; } diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index c7c9b8b28..27c07c538 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -24,6 +24,7 @@ + @@ -58,21 +59,21 @@ function initTable() { ele: $('#user_list_table'), columnDefs: [ {targets: 1, createdCell: function (td, cellData, rowData) { - var detail_btn = '' + cellData + ''; + var detail_btn = '' + escape(cellData) + ''; $(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id)); }}, {targets: 4, createdCell: function (td, cellData) { var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData; $(td).html('' + innerHtml + ''); }}, - {targets: 5, createdCell: function (td, cellData) { + {targets: 6, createdCell: function (td, cellData) { if (!cellData) { $(td).html('') } else { $(td).html('') } }}, - {targets: 6, createdCell: function (td, cellData, rowData) { + {targets: 7, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace('00000000-0000-0000-0000-000000000000', cellData); var del_btn = ""; @@ -90,7 +91,7 @@ function initTable() { ajax_url: '{% url "api-users:user-list" %}', columns: [ {data: "id"}, {data: "name" }, {data: "username" }, {data: "get_role_display" }, - {data: "groups_display" }, {data: "is_valid" }, {data: "id" } + {data: "groups_display" }, {data: "get_source_display" }, {data: "is_valid" }, {data: "id" } ], op_html: $('#actions').html() }; diff --git a/apps/users/templates/users/user_password_update.html b/apps/users/templates/users/user_password_update.html index 3dbe727a7..50c428ee6 100644 --- a/apps/users/templates/users/user_password_update.html +++ b/apps/users/templates/users/user_password_update.html @@ -7,6 +7,8 @@ + +
    {% trans 'Replay session' %}: - +
    {% trans 'System user' %} {% trans 'Remote addr' %} {% trans 'Protocol' %}{% trans 'Login from' %} {% trans 'Command' %} {% trans 'Date start' %}{% trans 'Date last active' %}{{ session.system_user }} {{ session.remote_addr|default:"" }} {{ session.protocol }}{{ session.get_login_from_display }} {{ session.id | get_session_command_amount }} {{ session.date_start }}{{ session.date_start|time_util_with_seconds:session.date_end }} {% if session.is_finished %} - {% trans "Replay" %} + {% trans "Replay" %} {% else %} - {% trans "Terminate" %} + {% if session.protocol == 'rdp' %} + {% trans "Terminate" %} + {% else %} + {% trans "Terminate" %} + {% endif %} {% endif %}
    {% trans 'UA' %} {% trans 'IP' %} {% trans 'City' %}{% trans 'MFA' %}{% trans 'Reason' %}{% trans 'Status' %} {% trans 'Date' %} {{ login_log.ip }} {{ login_log.city }}{{ login_log.get_mfa_display }}{{ login_log.get_reason_display }}{{ login_log.get_status_display }} {{ login_log.datetime }}
    {% trans 'Source' %}:{{ user_object.get_source_display }}
    {% trans 'Date expired' %}: {{ user_object.date_expired|date:"Y-m-j H:i:s" }}{% trans 'Username' %} {% trans 'Role' %} {% trans 'User group' %}{% trans 'Source' %} {% trans 'Active' %} {% trans 'Action' %}