mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-06-29 08:17:17 +00:00
合并冲突解决
This commit is contained in:
commit
5d63d2369f
14
README.md
14
README.md
@ -19,20 +19,14 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
|
||||
----
|
||||
|
||||
### 功能
|
||||
- 统一认证
|
||||
- 资产管理
|
||||
- 统一授权
|
||||
- 审计
|
||||
- 支持LDAP认证
|
||||
- Web terminal
|
||||
- SSH Server
|
||||
- 支持Windows RDP
|
||||
|
||||

|
||||
|
||||
### 开始使用
|
||||
|
||||
快速开始文档 [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)
|
||||
|
||||
|
@ -2,4 +2,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__version__ = "1.2.1"
|
||||
__version__ = "1.3.2"
|
||||
|
@ -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)
|
@ -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):
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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={
|
||||
|
@ -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'),
|
||||
'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.')
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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'];
|
||||
});
|
||||
|
@ -36,12 +36,13 @@
|
||||
{% endif %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% 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" %}
|
||||
|
||||
<h3 id="auth_title_id">{% trans 'Auth' %}</h3>
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
<div class="auto-generate">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||
@ -55,7 +56,7 @@
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.as_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||
<label for="{{ form.auto_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_push}}
|
||||
</div>
|
||||
@ -79,43 +80,86 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
|
||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var password_id = '#' + '{{ form.password.id_for_label }}';
|
||||
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
||||
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
||||
<script>
|
||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var login_mode_id = '#' + '{{ form.login_mode.id_for_label }}';
|
||||
|
||||
var need_change_field = [auto_generate_key, private_key_id, sudo_id, shell_id] ;
|
||||
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
|
||||
var password_id = '#' + '{{ form.password.id_for_label }}';
|
||||
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||
var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
|
||||
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
||||
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
||||
|
||||
function authFieldsDisplay() {
|
||||
if ($(auto_generate_key).prop('checked')) {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
} else {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
}
|
||||
var need_change_field = [
|
||||
auto_generate_key, private_key_id, auto_push_id, sudo_id, shell_id
|
||||
];
|
||||
var need_change_field_login_mode = [
|
||||
auto_generate_key, private_key_id, auto_push_id, password_id
|
||||
];
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
});
|
||||
}
|
||||
else if ($(protocol_id + " option:selected").text() === 'telnet (beta)') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
});
|
||||
}
|
||||
else {
|
||||
if($(login_mode_id).val() === 'manual'){
|
||||
$(sudo_id).closest('.form-group').removeClass('hidden');
|
||||
$(shell_id).closest('.form-group').removeClass('hidden');
|
||||
return
|
||||
}
|
||||
authFieldsDisplay();
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').removeClass('hidden')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id).attr('value') === 'rdp') {
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).addClass('hidden')
|
||||
});
|
||||
$(password_id).removeClass('hidden')
|
||||
} else {
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).removeClass('hidden')
|
||||
});
|
||||
}
|
||||
}
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
authFieldsDisplay();
|
||||
protocolChange();
|
||||
$(auto_generate_key).change(function () {
|
||||
authFieldsDisplay();
|
||||
});
|
||||
function authFieldsDisplay() {
|
||||
if ($(auto_generate_key).prop('checked')) {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
} else {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
function loginModeChange(){
|
||||
if ($(login_mode_id).val() === 'manual'){
|
||||
$('#auth_title_id').addClass('hidden');
|
||||
$.each(need_change_field_login_mode, function(index, value){
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
})
|
||||
</script>
|
||||
}
|
||||
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();
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -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,
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
管理用户是服务器的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||
管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||
Windows或其它硬件可以随意设置一个
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -107,6 +107,3 @@ $(document).ready(function(){
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -190,7 +190,7 @@
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Nodes' %}" id="groups_selected" class="select2 groups" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes_remain %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.name }}</option>
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
@ -204,7 +204,7 @@
|
||||
|
||||
{% for node in asset.nodes.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.name }}</b></td>
|
||||
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn-leave-node" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
|
@ -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 @@
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree" class="ztree">
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -87,7 +87,7 @@
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Hardware' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
{# <th class="text-center">{% trans 'Reachable' %}</th>#}
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -127,6 +127,9 @@
|
||||
<li class="divider"></li>
|
||||
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
|
||||
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
|
||||
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -157,26 +160,35 @@ function initTable() {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
if (cellData === 'Unknown'){
|
||||
$(td).html('<i class="fa fa-circle text-warning"></i>')
|
||||
} else if (!cellData) {
|
||||
$(td).html('<i class="fa fa-circle text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-circle text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
|
||||
{#{targets: 5, createdCell: function (td, cellData) {#}
|
||||
{# if (cellData === 'Unknown'){#}
|
||||
{# $(td).html('<i class="fa fa-circle text-warning"></i>')#}
|
||||
{# } else if (!cellData) {#}
|
||||
{# $(td).html('<i class="fa fa-circle text-danger"></i>')#}
|
||||
{# } else {#}
|
||||
{# $(td).html('<i class="fa fa-circle text-navy"></i>')#}
|
||||
{# }#}
|
||||
{# }},#}
|
||||
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:asset-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.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();
|
||||
|
@ -21,6 +21,7 @@
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% 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" %}
|
||||
|
@ -85,6 +85,9 @@ function initTable() {
|
||||
var update_btn = '<a href="{% url "assets:domain-gateway-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
if(rowData.protocol === 'rdp'){
|
||||
test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" disabled data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
}
|
||||
$(td).html(update_btn + test_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
@ -120,7 +123,6 @@ $(document).ready(function(){
|
||||
success_message: "可连接",
|
||||
fail_message: "连接失败"
|
||||
})
|
||||
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,6 +1,14 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block table_search %}{% endblock %}
|
||||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登
|
||||
录。
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
|
||||
@ -69,6 +77,3 @@ $(document).ready(function(){
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
@ -66,3 +66,28 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||
var port = '#' + '{{ form.port.id_for_label }}';
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||
{#$(port).val(3389);#}
|
||||
$(private_key_id).closest('.form-group').addClass('hidden')
|
||||
} else {
|
||||
{#$(port).val(22);#}
|
||||
$(private_key_id).closest('.form-group').removeClass('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
protocolChange();
|
||||
})
|
||||
.on('change', protocol_id, function(){
|
||||
protocolChange();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -63,15 +63,19 @@
|
||||
<td><b>{{ system_user.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Protocol' %}:</td>
|
||||
<td><b>{{ system_user.protocol }}</b></td>
|
||||
<td>{% trans 'Login mode' %}:</td>
|
||||
<td><b>{{ system_user.get_login_mode_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Protocol' %}:</td>
|
||||
<td><b id="id_protocol_type">{{ system_user.protocol }}</b></td>
|
||||
</tr>
|
||||
<tr class="only-ssh">
|
||||
<td>{% trans 'Sudo' %}:</td>
|
||||
<td><b>{{ system_user.sudo }}</b></td>
|
||||
</tr>
|
||||
{% if system_user.shell %}
|
||||
<tr>
|
||||
<tr class="only-ssh">
|
||||
<td>{% trans 'Shell' %}:</td>
|
||||
<td><b>{{ system_user.shell }}</b></td>
|
||||
</tr>
|
||||
@ -107,14 +111,14 @@
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel panel-primary ">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<tr class="only-ssh">
|
||||
<td width="50%">{% trans 'Auto push' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
@ -130,8 +134,8 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
{% if system_user.auto_push %}
|
||||
<tr class="only-ssh">
|
||||
<td width="50%">{% trans 'Push system user now' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
@ -139,8 +143,8 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% endif %}
|
||||
<tr class="only-ssh">
|
||||
<td width="50%">{% trans 'Test assets connective' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
@ -149,6 +153,15 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Clear auth' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# <tr>#}
|
||||
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
|
||||
{# <td>#}
|
||||
@ -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' %}"
|
||||
});
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -26,6 +26,7 @@
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||
<th class="text-center">{% trans 'Login mode' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Unreachable' %}</th>
|
||||
@ -48,7 +49,7 @@ function initTable() {
|
||||
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(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 = "<span class='text-navy'>" + cellData + "</span>";
|
||||
@ -57,7 +58,7 @@ function initTable() {
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
{targets: 7, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
|
||||
@ -66,7 +67,7 @@ function initTable() {
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
}},
|
||||
{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('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
|
||||
}},
|
||||
{targets: 9, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 10, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.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()
|
||||
|
@ -4,7 +4,6 @@
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
@ -15,10 +14,3 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
@ -23,8 +23,8 @@ urlpatterns = [
|
||||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||
url(r'^v1/assets/(?P<pk>[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<pk>[0-9a-zA-Z\-]{36})/gateway/$',
|
||||
api.AssetGatewayApi.as_view(), name='asset-gateway'),
|
||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$',
|
||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
url(r'^v1/admin-user/(?P<pk>[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<pk>[0-9a-zA-Z\-]{36})/connective/$',
|
||||
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'),
|
||||
url(r'^v1/nodes/(?P<pk>[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<pk>[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', api.NodeAssetsApi.as_view(), name='node-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/replace/$', api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$', api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$',
|
||||
api.NodeAddChildrenApi.as_view(), name='node-add-children'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$',
|
||||
api.NodeAssetsApi.as_view(), name='node-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$',
|
||||
api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/replace/$',
|
||||
api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$',
|
||||
api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$',
|
||||
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$',
|
||||
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
|
||||
|
||||
url(r'^v1/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
url(r'^v1/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$',
|
||||
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
@ -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,
|
||||
|
@ -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'),
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
||||
|
@ -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')
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -23,6 +23,9 @@
|
||||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
@ -23,6 +23,9 @@
|
||||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
@ -23,6 +23,9 @@
|
||||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
87
apps/common/templates/common/security_setting.html
Normal file
87
apps/common/templates/common/security_setting.html
Normal file
@ -0,0 +1,87 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'settings:basic-setting' %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Basic setting' %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-12" style="padding-left:0">
|
||||
<div class="ibox-content" style="border-width: 0;padding-top: 40px;">
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
<h3>{% trans "User login settings" %}</h3>
|
||||
{% for field in form %}
|
||||
{% if forloop.counter == 4 %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans "Password check rule" %}</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if not field.field|is_bool_field %}
|
||||
{% bootstrap_field field layout="horizontal" %}
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="col-sm-1">
|
||||
{{ field }}
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<span class="help-block" >{{ field.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
</script>
|
||||
{% endblock %}
|
@ -27,6 +27,9 @@
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i
|
||||
class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
@ -39,6 +42,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
<h3>{% trans "Basic setting" %}</h3>
|
||||
{% for field in form %}
|
||||
{% if not field.field|is_bool_field %}
|
||||
@ -60,6 +64,7 @@
|
||||
{% endfor %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
|
||||
<h3>{% trans "Command storage" %}</h3>
|
||||
<table class="table table-hover " id="task-history-list-table">
|
||||
<thead>
|
||||
|
@ -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'),
|
||||
]
|
||||
|
@ -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
|
||||
|
7
apps/common/validators.py
Normal file
7
apps/common/validators.py
Normal file
@ -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'))
|
@ -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)
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -19,7 +19,7 @@
|
||||
<a href="{% url 'ops:adhoc-history-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Output' %} </a>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Output' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,38 +2,25 @@
|
||||
<head>
|
||||
<title>term.js</title>
|
||||
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
|
||||
<style>
|
||||
html {
|
||||
background: #000;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
font: 20px/1.5 sans-serif;
|
||||
}
|
||||
.terminal {
|
||||
float: left;
|
||||
font-family: 'Monaco', 'Consolas', "DejaVu Sans Mono", "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
color: #f0f0f0;
|
||||
background-color: #555;
|
||||
padding: 20px 20px 20px;
|
||||
}
|
||||
.terminal-cursor {
|
||||
color: #000;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
.xterm-rows {
|
||||
{#padding: 15px;#}
|
||||
font-family: "Bitstream Vera Sans Mono", Monaco, "Consolas", Courier, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<div class="container">
|
||||
<div id="term">
|
||||
<div id="term" style="height: 100%;width: 100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="{% static 'js/term.js' %}"></script>
|
||||
<script>
|
||||
var rowHeight = 1;
|
||||
var colWidth = 1;
|
||||
var rowHeight = 18;
|
||||
var colWidth = 10;
|
||||
var mark = '';
|
||||
var url = "{% url 'api-ops:celery-task-log' pk=object.id %}";
|
||||
var term;
|
||||
@ -42,13 +29,16 @@
|
||||
var interval = 200;
|
||||
|
||||
function calWinSize() {
|
||||
var t = $('.terminal');
|
||||
rowHeight = 1.00 * t.height() / 24;
|
||||
colWidth = 1.00 * t.width() / 80;
|
||||
var t = $('#marker');
|
||||
{#rowHeight = 1.00 * t.height();#}
|
||||
{#colWidth = 1.00 * t.width() / 6;#}
|
||||
}
|
||||
function resize() {
|
||||
var rows = Math.floor(window.innerHeight / rowHeight) - 2;
|
||||
var cols = Math.floor(window.innerWidth / colWidth) - 10;
|
||||
{#console.log(rowHeight, window.innerHeight);#}
|
||||
{#console.log(colWidth, window.innerWidth);#}
|
||||
var rows = Math.floor(window.innerHeight / rowHeight) - 1;
|
||||
var cols = Math.floor(window.innerWidth / colWidth) - 2;
|
||||
console.log(rows, cols);
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
function requestAndWrite() {
|
||||
@ -74,21 +64,18 @@
|
||||
}
|
||||
}
|
||||
$(document).ready(function () {
|
||||
term = new Terminal({
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
useStyle: true,
|
||||
screenKeys: false,
|
||||
convertEol: false,
|
||||
cursorBlink: false
|
||||
});
|
||||
term.open();
|
||||
term.on('data', function (data) {
|
||||
term.write(data.replace('\r', '\r\n'))
|
||||
});
|
||||
calWinSize();
|
||||
term = new Terminal();
|
||||
term.open(document.getElementById('term'));
|
||||
term.resize(80, 24);
|
||||
resize();
|
||||
$('.terminal').detach().appendTo('#term');
|
||||
term.on('data', function (data) {
|
||||
{#term.write(data.replace('\r', '\r\n'))#}
|
||||
term.write(data);
|
||||
});
|
||||
window.onresize = function () {
|
||||
resize()
|
||||
};
|
||||
{#$('.terminal').detach().appendTo('#term');#}
|
||||
setInterval(function () {
|
||||
requestAndWrite()
|
||||
}, interval)
|
||||
|
@ -25,7 +25,7 @@
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -115,7 +115,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,
|
||||
|
@ -6,7 +6,7 @@ from rest_framework.views import APIView, Response
|
||||
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
|
||||
from rest_framework import viewsets
|
||||
|
||||
from common.utils import set_or_append_attr_bulk
|
||||
from common.utils import set_or_append_attr_bulk, get_object_or_none
|
||||
from users.permissions import IsValidUser, IsSuperUser, IsSuperUserOrAppUser
|
||||
from .utils import AssetPermissionUtil
|
||||
from .models import AssetPermission
|
||||
@ -41,11 +41,11 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
permissions = set(queryset.filter(assets=asset))
|
||||
for node in asset.nodes.all():
|
||||
inherit_nodes.update(set(node.ancestor_with_node))
|
||||
inherit_nodes.update(set(node.get_ancestor(with_self=True)))
|
||||
elif node_id:
|
||||
node = get_object_or_404(Node, pk=node_id)
|
||||
permissions = set(queryset.filter(nodes=node))
|
||||
inherit_nodes = node.ancestor
|
||||
inherit_nodes = node.get_ancestor()
|
||||
|
||||
for n in inherit_nodes:
|
||||
_permissions = queryset.filter(nodes=n)
|
||||
@ -70,11 +70,12 @@ class UserGrantedAssetsApi(ListAPIView):
|
||||
else:
|
||||
user = self.request.user
|
||||
|
||||
for k, v in AssetPermissionUtil.get_user_assets(user).items():
|
||||
util = AssetPermissionUtil(user)
|
||||
for k, v in util.get_assets().items():
|
||||
if k.is_unixlike():
|
||||
system_users_granted = [s for s in v if s.protocol == 'ssh']
|
||||
system_users_granted = [s for s in v if s.protocol in ['ssh', 'telnet']]
|
||||
else:
|
||||
system_users_granted = [s for s in v if s.protocol == 'rdp']
|
||||
system_users_granted = [s for s in v if s.protocol in ['rdp', 'telnet']]
|
||||
k.system_users_granted = system_users_granted
|
||||
queryset.append(k)
|
||||
return queryset
|
||||
@ -95,7 +96,8 @@ class UserGrantedNodesApi(ListAPIView):
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
else:
|
||||
user = self.request.user
|
||||
nodes = AssetPermissionUtil.get_user_nodes_with_assets(user)
|
||||
util = AssetPermissionUtil(user)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
return nodes.keys()
|
||||
|
||||
def get_permissions(self):
|
||||
@ -116,14 +118,15 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
|
||||
else:
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
|
||||
nodes = AssetPermissionUtil.get_user_nodes_with_assets(user)
|
||||
util = AssetPermissionUtil(user)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
for node, _assets in nodes.items():
|
||||
assets = _assets.keys()
|
||||
for k, v in _assets.items():
|
||||
if k.is_unixlike():
|
||||
system_users_granted = [s for s in v if s.protocol == 'ssh']
|
||||
system_users_granted = [s for s in v if s.protocol in ['ssh', 'telnet']]
|
||||
else:
|
||||
system_users_granted = [s for s in v if s.protocol == 'rdp']
|
||||
system_users_granted = [s for s in v if s.protocol in ['rdp', 'telnet']]
|
||||
k.system_users_granted = system_users_granted
|
||||
node.assets_granted = assets
|
||||
queryset.append(node)
|
||||
@ -147,8 +150,9 @@ class UserGrantedNodeAssetsApi(ListAPIView):
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
else:
|
||||
user = self.request.user
|
||||
util = AssetPermissionUtil(user)
|
||||
node = get_object_or_404(Node, id=node_id)
|
||||
nodes = AssetPermissionUtil.get_user_nodes_with_assets(user)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
assets = nodes.get(node, [])
|
||||
for asset, system_users in assets.items():
|
||||
asset.system_users_granted = system_users
|
||||
@ -172,7 +176,8 @@ class UserGroupGrantedAssetsApi(ListAPIView):
|
||||
return queryset
|
||||
|
||||
user_group = get_object_or_404(UserGroup, id=user_group_id)
|
||||
assets = AssetPermissionUtil.get_user_group_assets(user_group)
|
||||
util = AssetPermissionUtil(user_group)
|
||||
assets = util.get_assets()
|
||||
for k, v in assets.items():
|
||||
k.system_users_granted = v
|
||||
queryset.append(k)
|
||||
@ -189,7 +194,8 @@ class UserGroupGrantedNodesApi(ListAPIView):
|
||||
|
||||
if group_id:
|
||||
group = get_object_or_404(UserGroup, id=group_id)
|
||||
nodes = AssetPermissionUtil.get_user_group_nodes_with_assets(group)
|
||||
util = AssetPermissionUtil(group)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
return nodes.keys()
|
||||
return queryset
|
||||
|
||||
@ -206,7 +212,8 @@ class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
|
||||
return queryset
|
||||
|
||||
user_group = get_object_or_404(UserGroup, id=user_group_id)
|
||||
nodes = AssetPermissionUtil.get_user_group_nodes_with_assets(user_group)
|
||||
util = AssetPermissionUtil(user_group)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
for node, _assets in nodes.items():
|
||||
assets = _assets.keys()
|
||||
for asset, system_users in _assets.items():
|
||||
@ -226,7 +233,8 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView):
|
||||
|
||||
user_group = get_object_or_404(UserGroup, id=user_group_id)
|
||||
node = get_object_or_404(Node, id=node_id)
|
||||
nodes = AssetPermissionUtil.get_user_group_nodes_with_assets(user_group)
|
||||
util = AssetPermissionUtil(user_group)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
assets = nodes.get(node, [])
|
||||
for asset, system_users in assets.items():
|
||||
asset.system_users_granted = system_users
|
||||
@ -246,7 +254,8 @@ class ValidateUserAssetPermissionView(APIView):
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
system_user = get_object_or_404(SystemUser, id=system_id)
|
||||
|
||||
assets_granted = AssetPermissionUtil.get_user_assets(user)
|
||||
util = AssetPermissionUtil(user)
|
||||
assets_granted = util.get_assets()
|
||||
if system_user in assets_granted.get(asset, []):
|
||||
return Response({'msg': True}, status=200)
|
||||
else:
|
||||
|
@ -7,13 +7,23 @@ from django.utils import timezone
|
||||
from common.utils import date_expired_default, set_or_append_attr_bulk
|
||||
|
||||
|
||||
class ValidManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(is_active=True) \
|
||||
.filter(date_start__lt=timezone.now())\
|
||||
class AssetPermissionQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def valid(self):
|
||||
return self.active().filter(date_start__lt=timezone.now())\
|
||||
.filter(date_expired__gt=timezone.now())
|
||||
|
||||
|
||||
class AssetPermissionManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return AssetPermissionQuerySet(self.model, using=self._db)
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class AssetPermission(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
@ -23,14 +33,13 @@ class AssetPermission(models.Model):
|
||||
nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes"))
|
||||
system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', verbose_name=_("System user"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
||||
date_start = models.DateTimeField(default=timezone.now, verbose_name=_("Date start"))
|
||||
date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_('Date expired'))
|
||||
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start"))
|
||||
date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired'))
|
||||
created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(verbose_name=_('Comment'), blank=True)
|
||||
|
||||
objects = models.Manager()
|
||||
valid = ValidManager()
|
||||
objects = AssetPermissionManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -9,7 +9,7 @@ from common.fields import StringManyToManyField
|
||||
class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AssetPermission
|
||||
exclude = ('id', 'created_by', 'date_created')
|
||||
exclude = ('created_by', 'date_created')
|
||||
|
||||
|
||||
class AssetPermissionListSerializer(serializers.ModelSerializer):
|
||||
|
@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group {% if form.date_expired.errors or form.date_start.errors %} has-error {% endif %}" id="date_5">
|
||||
<label for="{{ form.date_expired.id_for_label }}" class="col-sm-2 control-label">{{ form.date_expired.label }}</label>
|
||||
<label for="{{ form.date_expired.id_for_label }}" class="col-sm-2 control-label">{% trans 'Validity period' %}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
|
@ -78,12 +78,12 @@ var zTree, table, show = 0;
|
||||
function onSelected(event, treeNode) {
|
||||
setCookie('node_selected', treeNode.id);
|
||||
var url = table.ajax.url();
|
||||
if (treeNode.is_asset) {
|
||||
url = setUrlParam(url, 'node', "");
|
||||
url = setUrlParam(url, 'asset', treeNode.id)
|
||||
} else {
|
||||
if (treeNode.is_node) {
|
||||
url = setUrlParam(url, 'asset', "");
|
||||
url = setUrlParam(url, 'node', treeNode.id)
|
||||
} else {
|
||||
url = setUrlParam(url, 'node', "");
|
||||
url = setUrlParam(url, 'asset', treeNode.id)
|
||||
}
|
||||
setCookie('node_selected', treeNode.id);
|
||||
table.ajax.url(url);
|
||||
@ -113,14 +113,14 @@ function filter(treeId, parentNode, childNodes) {
|
||||
$.each(childNodes, function (index, value) {
|
||||
value["pId"] = value["parent"];
|
||||
value["name"] = value["value"];
|
||||
value["isParent"] = value["assets_amount"] !== 0;
|
||||
value["iconSkin"] = value["is_asset"] ? "file" : null;
|
||||
value["isParent"] = value["is_node"];
|
||||
value["iconSkin"] = value["is_node"] ? null : 'file';
|
||||
});
|
||||
return childNodes;
|
||||
}
|
||||
|
||||
function beforeAsync(treeId, treeNode) {
|
||||
return true;
|
||||
return treeNode.is_node
|
||||
}
|
||||
|
||||
function makeLabel(data) {
|
||||
@ -226,7 +226,7 @@ function initTree() {
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: "{% url 'api-assets:node-children-2' %}?assets=1",
|
||||
url: "{% url 'api-assets:node-children-2' %}?assets=1&all=",
|
||||
autoParam:["id", "name=n", "level=lv"],
|
||||
dataFilter: filter,
|
||||
type: 'get'
|
||||
@ -238,18 +238,19 @@ function initTree() {
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-assets:node-children-2' %}", function(data, status){
|
||||
$.get("{% url 'api-assets:node-children-2' %}?assets=1&all=", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["pId"] = value["parent"];
|
||||
value["isParent"] = value["assets_amount"] !== 0;
|
||||
value["name"] = value["value"];
|
||||
value["open"] = value["key"] === "0";
|
||||
value["isParent"] = value["is_node"];
|
||||
value["iconSkin"] = value["is_node"] ? null : 'file';
|
||||
});
|
||||
zNodes = data;
|
||||
{#$.fn.zTree.init($("#assetTree"), setting);#}
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
selectQueryNode();
|
||||
{#selectQueryNode();#}
|
||||
});
|
||||
}
|
||||
|
||||
@ -286,10 +287,10 @@ $(document).ready(function(){
|
||||
var _nodes = [];
|
||||
var _assets = [];
|
||||
$.each(nodes, function (id, node) {
|
||||
if (node.is_asset) {
|
||||
_assets.push(node.id)
|
||||
} else {
|
||||
if (node.is_node) {
|
||||
_nodes.push(node.id)
|
||||
} else {
|
||||
_assets.push(node.id)
|
||||
}
|
||||
});
|
||||
url += "?assets=" + _assets.join(",") + "&nodes=" + _nodes.join(",");
|
||||
@ -303,6 +304,7 @@ $(document).ready(function(){
|
||||
|
||||
if (row.child.isShown()) {
|
||||
tr.removeClass('details');
|
||||
$(this).children('i:first-child').removeClass('fa-angle-down').addClass('fa-angle-right');
|
||||
row.child.hide();
|
||||
|
||||
// Remove from the 'open' array
|
||||
@ -310,7 +312,7 @@ $(document).ready(function(){
|
||||
}
|
||||
else {
|
||||
tr.addClass('details');
|
||||
$('.toggle i').removeClass('fa-angle-right').addClass('fa-angle-down');
|
||||
$(this).children('i:first-child').removeClass('fa-angle-right').addClass('fa-angle-down');
|
||||
row.child(format(row.data())).show();
|
||||
// Add to the 'open' array
|
||||
if ( idx === -1 ) {
|
||||
|
@ -1,300 +1,160 @@
|
||||
# coding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import collections
|
||||
from collections import defaultdict
|
||||
from django.utils import timezone
|
||||
import copy
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import set_or_append_attr_bulk, get_logger
|
||||
from common.utils import get_logger
|
||||
from .models import AssetPermission
|
||||
from .hands import Node
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class AssetPermissionUtil:
|
||||
class Tree:
|
||||
def __init__(self):
|
||||
self.__all_nodes = list(Node.objects.all().prefetch_related('assets'))
|
||||
self.__node_asset_map = defaultdict(set)
|
||||
self.nodes = defaultdict(dict)
|
||||
self.root = Node.root()
|
||||
self.init_node_asset_map()
|
||||
|
||||
@staticmethod
|
||||
def get_user_permissions(user):
|
||||
return AssetPermission.valid.all().filter(users=user)
|
||||
def init_node_asset_map(self):
|
||||
for node in self.__all_nodes:
|
||||
assets = node.get_assets().values_list('id', flat=True)
|
||||
for asset in assets:
|
||||
self.__node_asset_map[str(asset)].add(node)
|
||||
|
||||
@staticmethod
|
||||
def get_user_group_permissions(user_group):
|
||||
return AssetPermission.valid.all().filter(user_groups=user_group)
|
||||
def add_asset(self, asset, system_users):
|
||||
nodes = self.__node_asset_map.get(str(asset.id), [])
|
||||
self.add_nodes(nodes)
|
||||
for node in nodes:
|
||||
self.nodes[node][asset].update(system_users)
|
||||
|
||||
@staticmethod
|
||||
def get_asset_permissions(asset):
|
||||
return AssetPermission.valid.all().filter(assets=asset)
|
||||
def add_node(self, node):
|
||||
if node in self.nodes:
|
||||
return
|
||||
else:
|
||||
self.nodes[node] = defaultdict(set)
|
||||
if node.key == self.root.key:
|
||||
return
|
||||
parent_key = ':'.join(node.key.split(':')[:-1])
|
||||
for n in self.__all_nodes:
|
||||
if n.key == parent_key:
|
||||
self.add_node(n)
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def get_node_permissions(node):
|
||||
return AssetPermission.valid.all().filter(nodes=node)
|
||||
|
||||
@staticmethod
|
||||
def get_system_user_permissions(system_user):
|
||||
return AssetPermission.objects.all().filter(system_users=system_user)
|
||||
|
||||
@classmethod
|
||||
def get_user_group_nodes(cls, group):
|
||||
nodes = defaultdict(set)
|
||||
permissions = cls.get_user_group_permissions(group)
|
||||
for perm in permissions:
|
||||
_nodes = perm.nodes.all()
|
||||
_system_users = perm.system_users.all()
|
||||
set_or_append_attr_bulk(_nodes, 'permission', perm.id)
|
||||
for node in _nodes:
|
||||
nodes[node].update(set(_system_users))
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def get_user_group_assets_direct(cls, group):
|
||||
assets = defaultdict(set)
|
||||
permissions = cls.get_user_group_permissions(group)
|
||||
for perm in permissions:
|
||||
_assets = perm.assets.all()
|
||||
_system_users = perm.system_users.all()
|
||||
set_or_append_attr_bulk(_assets, 'permission', perm.id)
|
||||
for asset in _assets:
|
||||
assets[asset].update(set(_system_users))
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_group_nodes_assets(cls, group):
|
||||
assets = defaultdict(set)
|
||||
nodes = cls.get_user_group_nodes(group)
|
||||
for node, _system_users in nodes.items():
|
||||
_assets = node.get_all_assets()
|
||||
set_or_append_attr_bulk(_assets, 'inherit_node', node.id)
|
||||
set_or_append_attr_bulk(_assets, 'permission', getattr(node, 'permission', None))
|
||||
for asset in _assets:
|
||||
assets[asset].update(set(_system_users))
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_group_assets(cls, group):
|
||||
assets = defaultdict(set)
|
||||
_assets = cls.get_user_group_assets_direct(group)
|
||||
_nodes_assets = cls.get_user_group_nodes_assets(group)
|
||||
for asset, _system_users in _assets.items():
|
||||
assets[asset].update(set(_system_users))
|
||||
for asset, _system_users in _nodes_assets.items():
|
||||
assets[asset].update(set(_system_users))
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_group_nodes_with_assets(cls, user):
|
||||
"""
|
||||
:param user:
|
||||
:return: {node: {asset: set(su1, su2)}}
|
||||
"""
|
||||
nodes = defaultdict(dict)
|
||||
_assets = cls.get_user_group_assets(user)
|
||||
for asset, _system_users in _assets.items():
|
||||
_nodes = asset.get_nodes()
|
||||
for node in _nodes:
|
||||
if asset in nodes[node]:
|
||||
nodes[node][asset].update(_system_users)
|
||||
else:
|
||||
nodes[node][asset] = _system_users
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def get_user_assets_direct(cls, user):
|
||||
assets = defaultdict(set)
|
||||
permissions = list(cls.get_user_permissions(user))
|
||||
for perm in permissions:
|
||||
_assets = perm.assets.all()
|
||||
_system_users = perm.system_users.all()
|
||||
set_or_append_attr_bulk(_assets, 'permission', perm.id)
|
||||
for asset in _assets:
|
||||
assets[asset].update(set(_system_users))
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_nodes_direct(cls, user):
|
||||
nodes = defaultdict(set)
|
||||
permissions = cls.get_user_permissions(user)
|
||||
for perm in permissions:
|
||||
_nodes = perm.nodes.all()
|
||||
_system_users = perm.system_users.all()
|
||||
set_or_append_attr_bulk(_nodes, 'permission', perm.id)
|
||||
for node in _nodes:
|
||||
nodes[node].update(set(_system_users))
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def get_user_nodes_assets_direct(cls, user):
|
||||
assets = defaultdict(set)
|
||||
nodes = cls.get_user_nodes_direct(user)
|
||||
for node, _system_users in nodes.items():
|
||||
_assets = node.get_all_assets()
|
||||
set_or_append_attr_bulk(_assets, 'inherit_node', node.id)
|
||||
set_or_append_attr_bulk(_assets, 'permission', getattr(node, 'permission', None))
|
||||
for asset in _assets:
|
||||
assets[asset].update(set(_system_users))
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_assets_inherit_group(cls, user):
|
||||
assets = defaultdict(set)
|
||||
for group in user.groups.all():
|
||||
_assets = cls.get_user_group_assets(group)
|
||||
set_or_append_attr_bulk(_assets, 'inherit_group', group.id)
|
||||
for asset, _system_users in _assets.items():
|
||||
assets[asset].update(_system_users)
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_assets(cls, user):
|
||||
assets = defaultdict(set)
|
||||
_assets_direct = cls.get_user_assets_direct(user)
|
||||
_nodes_assets_direct = cls.get_user_nodes_assets_direct(user)
|
||||
_assets_inherit_group = cls.get_user_assets_inherit_group(user)
|
||||
for asset, _system_users in _assets_direct.items():
|
||||
assets[asset].update(_system_users)
|
||||
for asset, _system_users in _nodes_assets_direct.items():
|
||||
assets[asset].update(_system_users)
|
||||
for asset, _system_users in _assets_inherit_group.items():
|
||||
assets[asset].update(_system_users)
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_nodes_with_assets(cls, user):
|
||||
"""
|
||||
:param user:
|
||||
:return: {node: {asset: set(su1, su2)}}
|
||||
"""
|
||||
nodes = defaultdict(dict)
|
||||
_assets = cls.get_user_assets(user)
|
||||
for asset, _system_users in _assets.items():
|
||||
_nodes = asset.get_nodes()
|
||||
for node in _nodes:
|
||||
if asset in nodes[node]:
|
||||
nodes[node][asset].update(_system_users)
|
||||
else:
|
||||
nodes[node][asset] = _system_users
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def get_system_user_assets(cls, system_user):
|
||||
assets = set()
|
||||
permissions = cls.get_system_user_permissions(system_user)
|
||||
for perm in permissions:
|
||||
assets.update(set(perm.assets.all()))
|
||||
nodes = perm.nodes.all()
|
||||
for node in nodes:
|
||||
assets.update(set(node.get_all_assets()))
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_node_system_users(cls, node):
|
||||
system_users = set()
|
||||
permissions = cls.get_node_permissions(node)
|
||||
for perm in permissions:
|
||||
system_users.update(perm.system_users.all())
|
||||
return system_users
|
||||
def add_nodes(self, nodes):
|
||||
for node in nodes:
|
||||
self.add_node(node)
|
||||
|
||||
|
||||
# Abandon
|
||||
class NodePermissionUtil:
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_user_group_permissions(user_group):
|
||||
return user_group.nodepermission_set.all() \
|
||||
.filter(is_active=True) \
|
||||
.filter(date_expired__gt=timezone.now())
|
||||
|
||||
@staticmethod
|
||||
def get_system_user_permissions(system_user):
|
||||
return system_user.nodepermission_set.all() \
|
||||
.filter(is_active=True) \
|
||||
.filter(date_expired__gt=timezone.now())
|
||||
|
||||
@classmethod
|
||||
def get_user_group_nodes(cls, user_group):
|
||||
"""
|
||||
获取用户组授权的node和系统用户
|
||||
:param user_group:
|
||||
:return: {"node": set(systemuser1, systemuser2), ..}
|
||||
"""
|
||||
permissions = cls.get_user_group_permissions(user_group)
|
||||
nodes_directed = collections.defaultdict(set)
|
||||
|
||||
for perm in permissions:
|
||||
nodes_directed[perm.node].add(perm.system_user)
|
||||
|
||||
nodes = copy.deepcopy(nodes_directed)
|
||||
for node, system_users in nodes_directed.items():
|
||||
for child in node.get_family():
|
||||
nodes[child].update(system_users)
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def get_user_group_nodes_with_assets(cls, user_group):
|
||||
"""
|
||||
获取用户组授权的节点和系统用户,节点下带有资产
|
||||
:param user_group:
|
||||
:return: {"node": {"assets": "", "system_user": ""}, {}}
|
||||
"""
|
||||
nodes = cls.get_user_group_nodes(user_group)
|
||||
nodes_with_assets = dict()
|
||||
for node, system_users in nodes.items():
|
||||
nodes_with_assets[node] = {
|
||||
'assets': node.get_active_assets(),
|
||||
'system_users': system_users
|
||||
}
|
||||
return nodes_with_assets
|
||||
|
||||
@classmethod
|
||||
def get_user_group_assets(cls, user_group):
|
||||
assets = collections.defaultdict(set)
|
||||
permissions = cls.get_user_group_permissions(user_group)
|
||||
|
||||
for perm in permissions:
|
||||
for asset in perm.node.get_all_assets():
|
||||
assets[asset].add(perm.system_user)
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_user_nodes(cls, user):
|
||||
nodes = collections.defaultdict(set)
|
||||
def get_user_permissions(user, include_group=True):
|
||||
if include_group:
|
||||
groups = user.groups.all()
|
||||
for group in groups:
|
||||
group_nodes = cls.get_user_group_nodes(group)
|
||||
for node, system_users in group_nodes.items():
|
||||
nodes[node].update(system_users)
|
||||
arg = Q(users=user) | Q(user_groups__in=groups)
|
||||
else:
|
||||
arg = Q(users=user)
|
||||
return AssetPermission.objects.all().valid().filter(arg)
|
||||
|
||||
|
||||
def get_user_group_permissions(user_group):
|
||||
return AssetPermission.objects.all().valid().filter(
|
||||
user_groups=user_group
|
||||
)
|
||||
|
||||
|
||||
def get_asset_permissions(asset, include_node=True):
|
||||
if include_node:
|
||||
nodes = asset.get_all_nodes(flat=True)
|
||||
arg = Q(assets=asset) | Q(nodes__in=nodes)
|
||||
else:
|
||||
arg = Q(assets=asset)
|
||||
return AssetPermission.objects.all().valid().filter(arg)
|
||||
|
||||
|
||||
def get_node_permissions(node):
|
||||
return AssetPermission.objects.all().valid().filter(nodes=node)
|
||||
|
||||
|
||||
def get_system_user_permissions(system_user):
|
||||
return AssetPermission.objects.valid().all().filter(
|
||||
system_users=system_user
|
||||
)
|
||||
|
||||
|
||||
class AssetPermissionUtil:
|
||||
get_permissions_map = {
|
||||
"User": get_user_permissions,
|
||||
"UserGroup": get_user_group_permissions,
|
||||
"Asset": get_asset_permissions,
|
||||
"Node": get_node_permissions,
|
||||
"SystemUser": get_node_permissions,
|
||||
}
|
||||
|
||||
def __init__(self, obj):
|
||||
self.object = obj
|
||||
self._permissions = None
|
||||
self._assets = None
|
||||
|
||||
@property
|
||||
def permissions(self):
|
||||
if self._permissions:
|
||||
return self._permissions
|
||||
object_cls = self.object.__class__.__name__
|
||||
func = self.get_permissions_map[object_cls]
|
||||
permissions = func(self.object)
|
||||
self._permissions = permissions
|
||||
return permissions
|
||||
|
||||
def get_nodes_direct(self):
|
||||
"""
|
||||
返回用户/组授权规则直接关联的节点
|
||||
:return: {node1: set(system_user1,)}
|
||||
"""
|
||||
nodes = defaultdict(set)
|
||||
permissions = self.permissions.prefetch_related('nodes', 'system_users')
|
||||
for perm in permissions:
|
||||
for node in perm.nodes.all():
|
||||
nodes[node].update(perm.system_users.all())
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def get_user_nodes_with_assets(cls, user):
|
||||
nodes = cls.get_user_nodes(user)
|
||||
nodes_with_assets = dict()
|
||||
for node, system_users in nodes.items():
|
||||
nodes_with_assets[node] = {
|
||||
'assets': node.get_active_assets(),
|
||||
'system_users': system_users
|
||||
}
|
||||
return nodes_with_assets
|
||||
|
||||
@classmethod
|
||||
def get_user_assets(cls, user):
|
||||
assets = collections.defaultdict(set)
|
||||
nodes_with_assets = cls.get_user_nodes_with_assets(user)
|
||||
|
||||
for v in nodes_with_assets.values():
|
||||
for asset in v['assets']:
|
||||
assets[asset].update(v['system_users'])
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_system_user_assets(cls, system_user):
|
||||
assets = set()
|
||||
permissions = cls.get_system_user_permissions(system_user)
|
||||
|
||||
def get_assets_direct(self):
|
||||
"""
|
||||
返回用户授权规则直接关联的资产
|
||||
:return: {asset1: set(system_user1,)}
|
||||
"""
|
||||
assets = defaultdict(set)
|
||||
permissions = self.permissions.prefetch_related('assets', 'system_users')
|
||||
for perm in permissions:
|
||||
assets.update(perm.node.get_all_assets())
|
||||
for asset in perm.assets.all().valid().prefetch_related('nodes'):
|
||||
assets[asset].update(perm.system_users.all())
|
||||
return assets
|
||||
|
||||
def get_assets(self):
|
||||
if self._assets:
|
||||
return self._assets
|
||||
assets = self.get_assets_direct()
|
||||
nodes = self.get_nodes_direct()
|
||||
for node, system_users in nodes.items():
|
||||
_assets = node.get_all_assets().valid().prefetch_related('nodes')
|
||||
for asset in _assets:
|
||||
if isinstance(asset, Node):
|
||||
print(_assets)
|
||||
assets[asset].update(system_users)
|
||||
self._assets = assets
|
||||
return self._assets
|
||||
|
||||
def get_nodes_with_assets(self):
|
||||
"""
|
||||
返回节点并且包含资产
|
||||
{"node": {"assets": set("system_user")}}
|
||||
:return:
|
||||
"""
|
||||
assets = self.get_assets()
|
||||
tree = Tree()
|
||||
for asset, system_users in assets.items():
|
||||
tree.add_asset(asset, system_users)
|
||||
return tree.nodes
|
||||
|
||||
|
||||
|
@ -42,7 +42,7 @@ class AssetPermissionCreateView(AdminUserRequiredMixin, CreateView):
|
||||
|
||||
if nodes_id:
|
||||
nodes_id = nodes_id.split(",")
|
||||
nodes = Node.objects.filter(id__in=nodes_id)
|
||||
nodes = Node.objects.filter(id__in=nodes_id).exclude(id=Node.root().id)
|
||||
form['nodes'].initial = nodes
|
||||
if assets_id:
|
||||
assets_id = assets_id.split(",")
|
||||
|
@ -198,7 +198,8 @@ function objectDelete(obj, name, url, redirectTo) {
|
||||
}
|
||||
};
|
||||
var fail = function() {
|
||||
swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
||||
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
||||
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
@ -272,7 +273,7 @@ jumpserver.initDataTable = function (options) {
|
||||
$(td).html('<input type="checkbox" class="text-center ipt_check" id=99991937>'.replace('99991937', cellData));
|
||||
}
|
||||
},
|
||||
{className: 'text-center', targets: '_all'}
|
||||
{className: 'text-center', render: $.fn.dataTable.render.text(), targets: '_all'}
|
||||
];
|
||||
columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs;
|
||||
var select = {
|
||||
@ -609,3 +610,91 @@ function setUrlParam(url, name, value) {
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// 校验密码-改变规则颜色
|
||||
function checkPasswordRules(password, minLength) {
|
||||
if (wordMinLength(password, minLength)) {
|
||||
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordUpperCase(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', 'green');
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordLowerCase(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordNumber(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordSpecialChar(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', '#908a8a')
|
||||
}
|
||||
}
|
||||
|
||||
// 最小长度
|
||||
function wordMinLength(word, minLength) {
|
||||
//var minLength = {{ min_length }};
|
||||
var re = new RegExp("^(.{" + minLength + ",})$");
|
||||
return word.match(re)
|
||||
}
|
||||
// 大写字母
|
||||
function wordUpperCase(word) {
|
||||
return word.match(/([A-Z]+)/)
|
||||
}
|
||||
// 小写字母
|
||||
function wordLowerCase(word) {
|
||||
return word.match(/([a-z]+)/)
|
||||
}
|
||||
// 数字字符
|
||||
function wordNumber(word) {
|
||||
return word.match(/([\d]+)/)
|
||||
}
|
||||
// 特殊字符
|
||||
function wordSpecialChar(word) {
|
||||
return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/)
|
||||
}
|
||||
|
||||
// 显示弹窗密码规则
|
||||
function popoverPasswordRules(password_check_rules, $el) {
|
||||
var message = "";
|
||||
jQuery.each(password_check_rules, function (idx, rules) {
|
||||
message += "<li id=" + rules.id + " style='list-style-type:none;'> <i class='fa fa-check-circle-o' style='margin-right:10px;' ></i>" + rules.label + "</li>";
|
||||
});
|
||||
//$('#id_password_rules').html(message);
|
||||
$el.html(message)
|
||||
}
|
||||
|
||||
// 初始化弹窗popover
|
||||
function initPopover($container, $progress, $idPassword, $el, password_check_rules){
|
||||
options = {};
|
||||
// User Interface
|
||||
options.ui = {
|
||||
container: $container,
|
||||
viewports: {
|
||||
progress: $progress
|
||||
//errors: $('.popover-content')
|
||||
},
|
||||
showProgressbar: true,
|
||||
showVerdictsInsideProgressBar: true
|
||||
};
|
||||
$idPassword.pwstrength(options);
|
||||
popoverPasswordRules(password_check_rules, $el);
|
||||
}
|
||||
|
2261
apps/static/js/plugins/xterm/xterm.css
Normal file
2261
apps/static/js/plugins/xterm/xterm.css
Normal file
File diff suppressed because it is too large
Load Diff
5132
apps/static/js/plugins/xterm/xterm.js
Normal file
5132
apps/static/js/plugins/xterm/xterm.js
Normal file
File diff suppressed because it is too large
Load Diff
1
apps/static/js/plugins/xterm/xterm.js.map
Normal file
1
apps/static/js/plugins/xterm/xterm.js.map
Normal file
File diff suppressed because one or more lines are too long
976
apps/static/js/pwstrength-bootstrap.js
Executable file
976
apps/static/js/pwstrength-bootstrap.js
Executable file
@ -0,0 +1,976 @@
|
||||
/*!
|
||||
* jQuery Password Strength plugin for Twitter Bootstrap
|
||||
* Version: 2.2.1
|
||||
*
|
||||
* Copyright (c) 2008-2013 Tane Piper
|
||||
* Copyright (c) 2013 Alejandro Blanco
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
|
||||
(function (jQuery) {
|
||||
// Source: src/i18n.js
|
||||
|
||||
|
||||
|
||||
|
||||
var i18n = {};
|
||||
|
||||
(function (i18n, i18next) {
|
||||
'use strict';
|
||||
|
||||
i18n.fallback = {
|
||||
"wordMinLength": "Your password is too short",
|
||||
"wordMaxLength": "Your password is too long",
|
||||
"wordInvalidChar": "Your password contains an invalid character",
|
||||
"wordNotEmail": "Do not use your email as your password",
|
||||
"wordSimilarToUsername": "Your password cannot contain your username",
|
||||
"wordTwoCharacterClasses": "Use different character classes",
|
||||
"wordRepetitions": "Too many repetitions",
|
||||
"wordSequences": "Your password contains sequences",
|
||||
"errorList": "Errors:",
|
||||
"veryWeak": "Very Weak",
|
||||
"weak": "Weak",
|
||||
"normal": "Normal",
|
||||
"medium": "Medium",
|
||||
"strong": "Strong",
|
||||
"veryStrong": "Very Strong"
|
||||
};
|
||||
|
||||
i18n.t = function (key) {
|
||||
var result = '';
|
||||
|
||||
// Try to use i18next.com
|
||||
if (i18next) {
|
||||
result = i18next.t(key);
|
||||
} else {
|
||||
// Fallback to english
|
||||
result = i18n.fallback[key];
|
||||
}
|
||||
|
||||
return result === key ? '' : result;
|
||||
};
|
||||
}(i18n, window.i18next));
|
||||
|
||||
// Source: src/rules.js
|
||||
|
||||
|
||||
|
||||
|
||||
var rulesEngine = {};
|
||||
|
||||
try {
|
||||
if (!jQuery && module && module.exports) {
|
||||
var jQuery = require("jquery"),
|
||||
jsdom = require("jsdom").jsdom;
|
||||
jQuery = jQuery(jsdom().defaultView);
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
(function ($, rulesEngine) {
|
||||
"use strict";
|
||||
var validation = {};
|
||||
|
||||
rulesEngine.forbiddenSequences = [
|
||||
"0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl",
|
||||
"zxcvbnm", "!@#$%^&*()_+"
|
||||
];
|
||||
|
||||
validation.wordNotEmail = function (options, word, score) {
|
||||
if (word.match(/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordMinLength = function (options, word, score) {
|
||||
var wordlen = word.length,
|
||||
lenScore = Math.pow(wordlen, options.rules.raisePower);
|
||||
if (wordlen < options.common.minChar) {
|
||||
lenScore = (lenScore + score);
|
||||
}
|
||||
return lenScore;
|
||||
};
|
||||
|
||||
validation.wordMaxLength = function (options, word, score) {
|
||||
var wordlen = word.length,
|
||||
lenScore = Math.pow(wordlen, options.rules.raisePower);
|
||||
if (wordlen > options.common.maxChar) {
|
||||
return score;
|
||||
}
|
||||
return lenScore;
|
||||
};
|
||||
|
||||
validation.wordInvalidChar = function (options, word, score) {
|
||||
if (options.common.invalidCharsRegExp.test(word)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordMinLengthStaticScore = function (options, word, score) {
|
||||
return word.length < options.common.minChar ? 0 : score;
|
||||
};
|
||||
|
||||
validation.wordMaxLengthStaticScore = function (options, word, score) {
|
||||
return word.length > options.common.maxChar ? 0 : score;
|
||||
};
|
||||
|
||||
|
||||
validation.wordSimilarToUsername = function (options, word, score) {
|
||||
var username = $(options.common.usernameField).val();
|
||||
if (username && word.toLowerCase().match(username.replace(/[\-\[\]\/\{\}\(\)\*\+\=\?\:\.\\\^\$\|\!\,]/g, "\\$&").toLowerCase())) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordTwoCharacterClasses = function (options, word, score) {
|
||||
if (word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) ||
|
||||
(word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) ||
|
||||
(word.match(/(.[!,@,#,$,%,\^,&,*,?,_,~])/) && word.match(/[a-zA-Z0-9_]/))) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordRepetitions = function (options, word, score) {
|
||||
if (word.match(/(.)\1\1/)) { return score; }
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordSequences = function (options, word, score) {
|
||||
var found = false,
|
||||
j;
|
||||
if (word.length > 2) {
|
||||
$.each(rulesEngine.forbiddenSequences, function (idx, seq) {
|
||||
if (found) { return; }
|
||||
var sequences = [seq, seq.split('').reverse().join('')];
|
||||
$.each(sequences, function (idx, sequence) {
|
||||
for (j = 0; j < (word.length - 2); j += 1) { // iterate the word trough a sliding window of size 3:
|
||||
if (sequence.indexOf(word.toLowerCase().substring(j, j + 3)) > -1) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (found) { return score; }
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordLowercase = function (options, word, score) {
|
||||
return word.match(/[a-z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordUppercase = function (options, word, score) {
|
||||
return word.match(/[A-Z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneNumber = function (options, word, score) {
|
||||
return word.match(/\d+/) && score;
|
||||
};
|
||||
|
||||
validation.wordThreeNumbers = function (options, word, score) {
|
||||
return word.match(/(.*[0-9].*[0-9].*[0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneSpecialChar = function (options, word, score) {
|
||||
return word.match(/[!,@,#,$,%,\^,&,*,?,_,~]/) && score;
|
||||
};
|
||||
|
||||
validation.wordTwoSpecialChar = function (options, word, score) {
|
||||
return word.match(/(.*[!,@,#,$,%,\^,&,*,?,_,~].*[!,@,#,$,%,\^,&,*,?,_,~])/) && score;
|
||||
};
|
||||
|
||||
validation.wordUpperLowerCombo = function (options, word, score) {
|
||||
return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCombo = function (options, word, score) {
|
||||
return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCharCombo = function (options, word, score) {
|
||||
return word.match(/([a-zA-Z0-9].*[!,@,#,$,%,\^,&,*,?,_,~])|([!,@,#,$,%,\^,&,*,?,_,~].*[a-zA-Z0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordIsACommonPassword = function (options, word, score) {
|
||||
if ($.inArray(word, options.rules.commonPasswords) >= 0) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
rulesEngine.validation = validation;
|
||||
|
||||
rulesEngine.executeRules = function (options, word) {
|
||||
var totalScore = 0;
|
||||
|
||||
$.each(options.rules.activated, function (rule, active) {
|
||||
if (active) {
|
||||
var score = options.rules.scores[rule],
|
||||
funct = rulesEngine.validation[rule],
|
||||
result,
|
||||
errorMessage;
|
||||
|
||||
if (!$.isFunction(funct)) {
|
||||
funct = options.rules.extra[rule];
|
||||
}
|
||||
|
||||
if ($.isFunction(funct)) {
|
||||
result = funct(options, word, score);
|
||||
if (result) {
|
||||
totalScore += result;
|
||||
}
|
||||
if (result < 0 || (!$.isNumeric(result) && !result)) {
|
||||
errorMessage = options.ui.spanError(options, rule);
|
||||
if (errorMessage.length > 0) {
|
||||
options.instances.errors.push(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return totalScore;
|
||||
};
|
||||
}(jQuery, rulesEngine));
|
||||
|
||||
try {
|
||||
if (module && module.exports) {
|
||||
module.exports = rulesEngine;
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
// Source: src/options.js
|
||||
|
||||
|
||||
|
||||
|
||||
var defaultOptions = {};
|
||||
|
||||
defaultOptions.common = {};
|
||||
defaultOptions.common.minChar = 6;
|
||||
defaultOptions.common.maxChar = 20;
|
||||
defaultOptions.common.usernameField = "#username";
|
||||
defaultOptions.common.invalidCharsRegExp = new RegExp(/[\s,'"]/);
|
||||
defaultOptions.common.userInputs = [
|
||||
// Selectors for input fields with user input
|
||||
];
|
||||
defaultOptions.common.onLoad = undefined;
|
||||
defaultOptions.common.onKeyUp = undefined;
|
||||
defaultOptions.common.onScore = undefined;
|
||||
defaultOptions.common.zxcvbn = false;
|
||||
defaultOptions.common.zxcvbnTerms = [
|
||||
// List of disrecommended words
|
||||
];
|
||||
defaultOptions.common.events = ["keyup", "change", "paste"];
|
||||
defaultOptions.common.debug = false;
|
||||
|
||||
defaultOptions.rules = {};
|
||||
defaultOptions.rules.extra = {};
|
||||
defaultOptions.rules.scores = {
|
||||
wordNotEmail: -100,
|
||||
wordMinLength: -50,
|
||||
wordMaxLength: -50,
|
||||
wordInvalidChar: -100,
|
||||
wordSimilarToUsername: -100,
|
||||
wordSequences: -20,
|
||||
wordTwoCharacterClasses: 2,
|
||||
wordRepetitions: -25,
|
||||
wordLowercase: 1,
|
||||
wordUppercase: 3,
|
||||
wordOneNumber: 3,
|
||||
wordThreeNumbers: 5,
|
||||
wordOneSpecialChar: 3,
|
||||
wordTwoSpecialChar: 5,
|
||||
wordUpperLowerCombo: 2,
|
||||
wordLetterNumberCombo: 2,
|
||||
wordLetterNumberCharCombo: 2,
|
||||
wordIsACommonPassword: -100
|
||||
};
|
||||
defaultOptions.rules.activated = {
|
||||
wordNotEmail: true,
|
||||
wordMinLength: true,
|
||||
wordMaxLength: false,
|
||||
wordInvalidChar: false,
|
||||
wordSimilarToUsername: true,
|
||||
wordSequences: true,
|
||||
wordTwoCharacterClasses: true,
|
||||
wordRepetitions: true,
|
||||
wordLowercase: true,
|
||||
wordUppercase: true,
|
||||
wordOneNumber: true,
|
||||
wordThreeNumbers: true,
|
||||
wordOneSpecialChar: true,
|
||||
wordTwoSpecialChar: true,
|
||||
wordUpperLowerCombo: true,
|
||||
wordLetterNumberCombo: true,
|
||||
wordLetterNumberCharCombo: true,
|
||||
wordIsACommonPassword: true
|
||||
};
|
||||
defaultOptions.rules.raisePower = 1.4;
|
||||
// List taken from https://github.com/danielmiessler/SecLists (MIT License)
|
||||
defaultOptions.rules.commonPasswords = [
|
||||
'123456',
|
||||
'password',
|
||||
'12345678',
|
||||
'qwerty',
|
||||
'123456789',
|
||||
'12345',
|
||||
'1234',
|
||||
'111111',
|
||||
'1234567',
|
||||
'dragon',
|
||||
'123123',
|
||||
'baseball',
|
||||
'abc123',
|
||||
'football',
|
||||
'monkey',
|
||||
'letmein',
|
||||
'696969',
|
||||
'shadow',
|
||||
'master',
|
||||
'666666',
|
||||
'qwertyuiop',
|
||||
'123321',
|
||||
'mustang',
|
||||
'1234567890',
|
||||
'michael',
|
||||
'654321',
|
||||
'pussy',
|
||||
'superman',
|
||||
'1qaz2wsx',
|
||||
'7777777',
|
||||
'fuckyou',
|
||||
'121212',
|
||||
'000000',
|
||||
'qazwsx',
|
||||
'123qwe',
|
||||
'killer',
|
||||
'trustno1',
|
||||
'jordan',
|
||||
'jennifer',
|
||||
'zxcvbnm',
|
||||
'asdfgh',
|
||||
'hunter',
|
||||
'buster',
|
||||
'soccer',
|
||||
'harley',
|
||||
'batman',
|
||||
'andrew',
|
||||
'tigger',
|
||||
'sunshine',
|
||||
'iloveyou',
|
||||
'fuckme',
|
||||
'2000',
|
||||
'charlie',
|
||||
'robert',
|
||||
'thomas',
|
||||
'hockey',
|
||||
'ranger',
|
||||
'daniel',
|
||||
'starwars',
|
||||
'klaster',
|
||||
'112233',
|
||||
'george',
|
||||
'asshole',
|
||||
'computer',
|
||||
'michelle',
|
||||
'jessica',
|
||||
'pepper',
|
||||
'1111',
|
||||
'zxcvbn',
|
||||
'555555',
|
||||
'11111111',
|
||||
'131313',
|
||||
'freedom',
|
||||
'777777',
|
||||
'pass',
|
||||
'fuck',
|
||||
'maggie',
|
||||
'159753',
|
||||
'aaaaaa',
|
||||
'ginger',
|
||||
'princess',
|
||||
'joshua',
|
||||
'cheese',
|
||||
'amanda',
|
||||
'summer',
|
||||
'love',
|
||||
'ashley',
|
||||
'6969',
|
||||
'nicole',
|
||||
'chelsea',
|
||||
'biteme',
|
||||
'matthew',
|
||||
'access',
|
||||
'yankees',
|
||||
'987654321',
|
||||
'dallas',
|
||||
'austin',
|
||||
'thunder',
|
||||
'taylor',
|
||||
'matrix'
|
||||
];
|
||||
|
||||
defaultOptions.ui = {};
|
||||
defaultOptions.ui.bootstrap2 = false;
|
||||
defaultOptions.ui.bootstrap4 = false;
|
||||
defaultOptions.ui.colorClasses = [
|
||||
"danger", "danger", "danger", "warning", "warning", "success"
|
||||
];
|
||||
defaultOptions.ui.showProgressBar = true;
|
||||
defaultOptions.ui.progressBarEmptyPercentage = 1;
|
||||
defaultOptions.ui.progressBarMinPercentage = 1;
|
||||
defaultOptions.ui.progressExtraCssClasses = '';
|
||||
defaultOptions.ui.progressBarExtraCssClasses = '';
|
||||
defaultOptions.ui.showPopover = false;
|
||||
defaultOptions.ui.popoverPlacement = "bottom";
|
||||
defaultOptions.ui.showStatus = false;
|
||||
defaultOptions.ui.spanError = function (options, key) {
|
||||
"use strict";
|
||||
var text = options.i18n.t(key);
|
||||
if (!text) { return ''; }
|
||||
return '<span style="color: #d52929">' + text + '</span>';
|
||||
};
|
||||
defaultOptions.ui.popoverError = function (options) {
|
||||
"use strict";
|
||||
var errors = options.instances.errors,
|
||||
errorsTitle = options.i18n.t("errorList"),
|
||||
message = "<div>" + errorsTitle + "<ul class='error-list' style='margin-bottom: 0;'>";
|
||||
|
||||
jQuery.each(errors, function (idx, err) {
|
||||
message += "<li>" + err + "</li>";
|
||||
});
|
||||
message += "</ul></div>";
|
||||
return message;
|
||||
};
|
||||
defaultOptions.ui.showVerdicts = true;
|
||||
defaultOptions.ui.showVerdictsInsideProgressBar = false;
|
||||
defaultOptions.ui.useVerdictCssClass = false;
|
||||
defaultOptions.ui.showErrors = false;
|
||||
defaultOptions.ui.showScore = false;
|
||||
defaultOptions.ui.container = undefined;
|
||||
defaultOptions.ui.viewports = {
|
||||
progress: undefined,
|
||||
verdict: undefined,
|
||||
errors: undefined,
|
||||
score: undefined
|
||||
};
|
||||
defaultOptions.ui.scores = [0, 14, 26, 38, 50];
|
||||
|
||||
defaultOptions.i18n = {};
|
||||
defaultOptions.i18n.t = i18n.t;
|
||||
|
||||
// Source: src/ui.js
|
||||
|
||||
|
||||
|
||||
|
||||
var ui = {};
|
||||
|
||||
(function ($, ui) {
|
||||
"use strict";
|
||||
|
||||
var statusClasses = ["error", "warning", "success"],
|
||||
verdictKeys = [
|
||||
"veryWeak", "weak", "normal", "medium", "strong", "veryStrong"
|
||||
];
|
||||
|
||||
ui.getContainer = function (options, $el) {
|
||||
var $container;
|
||||
|
||||
$container = $(options.ui.container);
|
||||
if (!($container && $container.length === 1)) {
|
||||
$container = $el.parent();
|
||||
}
|
||||
return $container;
|
||||
};
|
||||
|
||||
ui.findElement = function ($container, viewport, cssSelector) {
|
||||
if (viewport) {
|
||||
return $container.find(viewport).find(cssSelector);
|
||||
}
|
||||
return $container.find(cssSelector);
|
||||
};
|
||||
|
||||
ui.getUIElements = function (options, $el) {
|
||||
var $container, result;
|
||||
|
||||
if (options.instances.viewports) {
|
||||
return options.instances.viewports;
|
||||
}
|
||||
|
||||
$container = ui.getContainer(options, $el);
|
||||
|
||||
result = {};
|
||||
result.$progressbar = ui.findElement($container, options.ui.viewports.progress, "div.progress");
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = result.$progressbar.find("span.password-verdict");
|
||||
}
|
||||
|
||||
if (!options.ui.showPopover) {
|
||||
if (!options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = ui.findElement($container, options.ui.viewports.verdict, "span.password-verdict");
|
||||
}
|
||||
result.$errors = ui.findElement($container, options.ui.viewports.errors, "ul.error-list");
|
||||
}
|
||||
result.$score = ui.findElement($container, options.ui.viewports.score,
|
||||
"span.password-score");
|
||||
|
||||
options.instances.viewports = result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.initProgressBar = function (options, $el) {
|
||||
var $container = ui.getContainer(options, $el),
|
||||
progressbar = "<div class='progress ";
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
// Boostrap 2
|
||||
progressbar += options.ui.progressBarExtraCssClasses +
|
||||
"'><div class='";
|
||||
} else {
|
||||
// Bootstrap 3 & 4
|
||||
progressbar += options.ui.progressExtraCssClasses + "'><div class='" +
|
||||
options.ui.progressBarExtraCssClasses + " progress-";
|
||||
}
|
||||
progressbar += "bar'>";
|
||||
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
progressbar += "<span class='password-verdict'></span>";
|
||||
}
|
||||
|
||||
progressbar += "</div></div>";
|
||||
|
||||
if (options.ui.viewports.progress) {
|
||||
$container.find(options.ui.viewports.progress).append(progressbar);
|
||||
} else {
|
||||
$(progressbar).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initHelper = function (options, $el, html, viewport) {
|
||||
var $container = ui.getContainer(options, $el);
|
||||
if (viewport) {
|
||||
$container.find(viewport).append(html);
|
||||
} else {
|
||||
$(html).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initVerdict = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<span class='password-verdict'></span>",
|
||||
options.ui.viewports.verdict);
|
||||
};
|
||||
|
||||
ui.initErrorList = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<ul class='error-list'></ul >",
|
||||
options.ui.viewports.errors);
|
||||
};
|
||||
|
||||
ui.initScore = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<span class='password-score'></span>",
|
||||
options.ui.viewports.score);
|
||||
};
|
||||
|
||||
ui.initPopover = function (options, $el) {
|
||||
$el.popover("destroy");
|
||||
$el.popover({
|
||||
html: true,
|
||||
placement: options.ui.popoverPlacement,
|
||||
trigger: "manual",
|
||||
content: " "
|
||||
});
|
||||
};
|
||||
|
||||
ui.initUI = function (options, $el) {
|
||||
if (options.ui.showPopover) {
|
||||
ui.initPopover(options, $el);
|
||||
} else {
|
||||
if (options.ui.showErrors) { ui.initErrorList(options, $el); }
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.initVerdict(options, $el);
|
||||
}
|
||||
}
|
||||
if (options.ui.showProgressBar) {
|
||||
ui.initProgressBar(options, $el);
|
||||
}
|
||||
if (options.ui.showScore) {
|
||||
ui.initScore(options, $el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateProgressBar = function (options, $el, cssClass, percentage) {
|
||||
var $progressbar = ui.getUIElements(options, $el).$progressbar,
|
||||
$bar = $progressbar.find(".progress-bar"),
|
||||
cssPrefix = "progress-";
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
$bar = $progressbar.find(".bar");
|
||||
cssPrefix = "";
|
||||
}
|
||||
|
||||
$.each(options.ui.colorClasses, function (idx, value) {
|
||||
if (options.ui.bootstrap4) {
|
||||
$bar.removeClass("bg-" + value);
|
||||
} else {
|
||||
$bar.removeClass(cssPrefix + "bar-" + value);
|
||||
}
|
||||
});
|
||||
if (options.ui.bootstrap4) {
|
||||
$bar.addClass("bg-" + options.ui.colorClasses[cssClass]);
|
||||
} else {
|
||||
$bar.addClass(cssPrefix + "bar-" + options.ui.colorClasses[cssClass]);
|
||||
}
|
||||
$bar.css("width", percentage + '%');
|
||||
};
|
||||
|
||||
ui.updateVerdict = function (options, $el, cssClass, text) {
|
||||
var $verdict = ui.getUIElements(options, $el).$verdict;
|
||||
$verdict.removeClass(options.ui.colorClasses.join(' '));
|
||||
if (cssClass > -1) {
|
||||
$verdict.addClass(options.ui.colorClasses[cssClass]);
|
||||
}
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
$verdict.css('white-space', 'nowrap');
|
||||
}
|
||||
$verdict.html(text);
|
||||
};
|
||||
|
||||
ui.updateErrors = function (options, $el, remove) {
|
||||
var $errors = ui.getUIElements(options, $el).$errors,
|
||||
html = "";
|
||||
|
||||
if (!remove) {
|
||||
$.each(options.instances.errors, function (idx, err) {
|
||||
html += "<li style='list-style-type:none;'>" + err + "</li>";
|
||||
});
|
||||
}
|
||||
$errors.html(html);
|
||||
};
|
||||
|
||||
ui.updateScore = function (options, $el, score, remove) {
|
||||
var $score = ui.getUIElements(options, $el).$score,
|
||||
html = "";
|
||||
|
||||
if (!remove) { html = score.toFixed(2); }
|
||||
$score.html(html);
|
||||
};
|
||||
|
||||
ui.updatePopover = function (options, $el, verdictText, remove) {
|
||||
var popover = $el.data("bs.popover"),
|
||||
html = "",
|
||||
hide = true;
|
||||
|
||||
if (options.ui.showVerdicts &&
|
||||
!options.ui.showVerdictsInsideProgressBar &&
|
||||
verdictText.length > 0) {
|
||||
html = "<h5><span class='password-verdict'>" + verdictText +
|
||||
"</span></h5>";
|
||||
hide = false;
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
if (options.instances.errors.length > 0) {
|
||||
hide = false;
|
||||
}
|
||||
html += options.ui.popoverError(options);
|
||||
}
|
||||
|
||||
if (hide || remove) {
|
||||
$el.popover("hide");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.ui.bootstrap2) { popover = $el.data("popover"); }
|
||||
|
||||
if (popover.$arrow && popover.$arrow.parents("body").length > 0) {
|
||||
$el.find("+ .popover .popover-content").html(html);
|
||||
} else {
|
||||
// It's hidden
|
||||
popover.options.content = html;
|
||||
$el.popover("show");
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateFieldStatus = function (options, $el, cssClass, remove) {
|
||||
var targetClass = options.ui.bootstrap2 ? ".control-group" : ".form-group",
|
||||
$container = $el.parents(targetClass).first();
|
||||
|
||||
$.each(statusClasses, function (idx, css) {
|
||||
if (!options.ui.bootstrap2) { css = "has-" + css; }
|
||||
$container.removeClass(css);
|
||||
});
|
||||
|
||||
if (remove) { return; }
|
||||
|
||||
cssClass = statusClasses[Math.floor(cssClass / 2)];
|
||||
if (!options.ui.bootstrap2) { cssClass = "has-" + cssClass; }
|
||||
$container.addClass(cssClass);
|
||||
};
|
||||
|
||||
ui.percentage = function (options, score, maximun) {
|
||||
var result = Math.floor(100 * score / maximun),
|
||||
min = options.ui.progressBarMinPercentage;
|
||||
|
||||
result = result <= min ? min : result;
|
||||
result = result > 100 ? 100 : result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.getVerdictAndCssClass = function (options, score) {
|
||||
var level, verdict;
|
||||
|
||||
if (score === undefined) { return ['', 0]; }
|
||||
|
||||
if (score <= options.ui.scores[0]) {
|
||||
level = 0;
|
||||
} else if (score < options.ui.scores[1]) {
|
||||
level = 1;
|
||||
} else if (score < options.ui.scores[2]) {
|
||||
level = 2;
|
||||
} else if (score < options.ui.scores[3]) {
|
||||
level = 3;
|
||||
} else if (score < options.ui.scores[4]) {
|
||||
level = 4;
|
||||
} else {
|
||||
level = 5;
|
||||
}
|
||||
|
||||
verdict = verdictKeys[level];
|
||||
|
||||
return [options.i18n.t(verdict), level];
|
||||
};
|
||||
|
||||
ui.updateUI = function (options, $el, score) {
|
||||
var cssClass, barPercentage, verdictText, verdictCssClass;
|
||||
|
||||
cssClass = ui.getVerdictAndCssClass(options, score);
|
||||
verdictText = score === 0 ? '' : cssClass[0];
|
||||
cssClass = cssClass[1];
|
||||
verdictCssClass = options.ui.useVerdictCssClass ? cssClass : -1;
|
||||
|
||||
if (options.ui.showProgressBar) {
|
||||
if (score === undefined) {
|
||||
barPercentage = options.ui.progressBarEmptyPercentage;
|
||||
} else {
|
||||
barPercentage = ui.percentage(options, score, options.ui.scores[4]);
|
||||
}
|
||||
ui.updateProgressBar(options, $el, cssClass, barPercentage);
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ui.showStatus) {
|
||||
ui.updateFieldStatus(options, $el, cssClass, score === undefined);
|
||||
}
|
||||
|
||||
if (options.ui.showPopover) {
|
||||
ui.updatePopover(options, $el, verdictText, score === undefined);
|
||||
} else {
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
ui.updateErrors(options, $el, score === undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ui.showScore) {
|
||||
ui.updateScore(options, $el, score, score === undefined);
|
||||
}
|
||||
};
|
||||
}(jQuery, ui));
|
||||
|
||||
// Source: src/methods.js
|
||||
|
||||
|
||||
|
||||
|
||||
var methods = {};
|
||||
|
||||
(function ($, methods) {
|
||||
"use strict";
|
||||
var onKeyUp, onPaste, applyToAll;
|
||||
|
||||
onKeyUp = function (event) {
|
||||
var $el = $(event.target),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
word = $el.val(),
|
||||
userInputs,
|
||||
verdictText,
|
||||
verdictLevel,
|
||||
score;
|
||||
|
||||
if (options === undefined) { return; }
|
||||
|
||||
options.instances.errors = [];
|
||||
if (word.length === 0) {
|
||||
score = undefined;
|
||||
} else {
|
||||
if (options.common.zxcvbn) {
|
||||
userInputs = [];
|
||||
$.each(options.common.userInputs.concat([options.common.usernameField]), function (idx, selector) {
|
||||
var value = $(selector).val();
|
||||
if (value) { userInputs.push(value); }
|
||||
});
|
||||
userInputs = userInputs.concat(options.common.zxcvbnTerms);
|
||||
score = zxcvbn(word, userInputs).guesses;
|
||||
score = Math.log(score) * Math.LOG2E;
|
||||
} else {
|
||||
score = rulesEngine.executeRules(options, word);
|
||||
}
|
||||
if ($.isFunction(options.common.onScore)) {
|
||||
score = options.common.onScore(options, word, score);
|
||||
}
|
||||
}
|
||||
ui.updateUI(options, $el, score);
|
||||
verdictText = ui.getVerdictAndCssClass(options, score);
|
||||
verdictLevel = verdictText[1];
|
||||
verdictText = verdictText[0];
|
||||
|
||||
if (options.common.debug) {
|
||||
console.log(score + ' - ' + verdictText);
|
||||
}
|
||||
|
||||
if ($.isFunction(options.common.onKeyUp)) {
|
||||
options.common.onKeyUp(event, {
|
||||
score: score,
|
||||
verdictText: verdictText,
|
||||
verdictLevel: verdictLevel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPaste = function (event) {
|
||||
// This handler is necessary because the paste event fires before the
|
||||
// content is actually in the input, so we cannot read its value right
|
||||
// away. Therefore, the timeouts.
|
||||
var $el = $(event.target),
|
||||
word = $el.val(),
|
||||
tries = 0,
|
||||
callback;
|
||||
|
||||
callback = function () {
|
||||
var newWord = $el.val();
|
||||
|
||||
if (newWord !== word) {
|
||||
onKeyUp(event);
|
||||
} else if (tries < 3) {
|
||||
tries += 1;
|
||||
setTimeout(callback, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(callback, 100);
|
||||
};
|
||||
|
||||
methods.init = function (settings) {
|
||||
this.each(function (idx, el) {
|
||||
// Make it deep extend (first param) so it extends also the
|
||||
// rules and other inside objects
|
||||
var clonedDefaults = $.extend(true, {}, defaultOptions),
|
||||
localOptions = $.extend(true, clonedDefaults, settings),
|
||||
$el = $(el);
|
||||
|
||||
localOptions.instances = {};
|
||||
$el.data("pwstrength-bootstrap", localOptions);
|
||||
|
||||
$.each(localOptions.common.events, function (idx, eventName) {
|
||||
var handler = eventName === "paste" ? onPaste : onKeyUp;
|
||||
$el.on(eventName, handler);
|
||||
});
|
||||
|
||||
ui.initUI(localOptions, $el);
|
||||
$el.trigger("keyup");
|
||||
|
||||
if ($.isFunction(localOptions.common.onLoad)) {
|
||||
localOptions.common.onLoad();
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
methods.destroy = function () {
|
||||
this.each(function (idx, el) {
|
||||
var $el = $(el),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
elements = ui.getUIElements(options, $el);
|
||||
elements.$progressbar.remove();
|
||||
elements.$verdict.remove();
|
||||
elements.$errors.remove();
|
||||
$el.removeData("pwstrength-bootstrap");
|
||||
});
|
||||
};
|
||||
|
||||
methods.forceUpdate = function () {
|
||||
this.each(function (idx, el) {
|
||||
var event = { target: el };
|
||||
onKeyUp(event);
|
||||
});
|
||||
};
|
||||
|
||||
methods.addRule = function (name, method, score, active) {
|
||||
this.each(function (idx, el) {
|
||||
var options = $(el).data("pwstrength-bootstrap");
|
||||
|
||||
options.rules.activated[name] = active;
|
||||
options.rules.scores[name] = score;
|
||||
options.rules.extra[name] = method;
|
||||
});
|
||||
};
|
||||
|
||||
applyToAll = function (rule, prop, value) {
|
||||
this.each(function (idx, el) {
|
||||
$(el).data("pwstrength-bootstrap").rules[prop][rule] = value;
|
||||
});
|
||||
};
|
||||
|
||||
methods.changeScore = function (rule, score) {
|
||||
applyToAll.call(this, rule, "scores", score);
|
||||
};
|
||||
|
||||
methods.ruleActive = function (rule, active) {
|
||||
applyToAll.call(this, rule, "activated", active);
|
||||
};
|
||||
|
||||
methods.ruleIsMet = function (rule) {
|
||||
if ($.isFunction(rulesEngine.validation[rule])) {
|
||||
if (rule === "wordMinLength") {
|
||||
rule = "wordMinLengthStaticScore";
|
||||
} else if (rule === "wordMaxLength") {
|
||||
rule = "wordMaxLengthStaticScore";
|
||||
}
|
||||
|
||||
var rulesMetCnt = 0;
|
||||
|
||||
this.each(function (idx, el) {
|
||||
var options = $(el).data("pwstrength-bootstrap");
|
||||
|
||||
rulesMetCnt += rulesEngine.validation[rule](options, $(el).val(), 1);
|
||||
});
|
||||
|
||||
return (rulesMetCnt === this.length);
|
||||
}
|
||||
|
||||
$.error("Rule " + rule + " does not exist on jQuery.pwstrength-bootstrap.validation");
|
||||
};
|
||||
|
||||
$.fn.pwstrength = function (method) {
|
||||
var result;
|
||||
|
||||
if (methods[method]) {
|
||||
result = methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
} else if (typeof method === "object" || !method) {
|
||||
result = methods.init.apply(this, arguments);
|
||||
} else {
|
||||
$.error("Method " + method + " does not exist on jQuery.pwstrength-bootstrap");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}(jQuery, methods));
|
||||
}(jQuery));
|
@ -5,6 +5,7 @@
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
{% block custom_head_css_js_create %} {% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="footer fixed">
|
||||
<div class="pull-right">
|
||||
Version <strong>1.2.1-{% include '_build.html' %}</strong> GPLv2.
|
||||
<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">
|
||||
Version <strong>1.3.2-{% include '_build.html' %}</strong> GPLv2.
|
||||
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
|
||||
</div>
|
||||
<div>
|
||||
<strong>Copyright</strong> 北京堆栈科技有限公司 © 2014-2018
|
||||
|
@ -9,6 +9,7 @@ from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils import timezone
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http.response import HttpResponseRedirectBase
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.conf import settings
|
||||
|
||||
@ -25,7 +26,7 @@ from .serializers import TerminalSerializer, StatusSerializer, \
|
||||
SessionSerializer, TaskSerializer, ReplaySerializer
|
||||
from .hands import IsSuperUserOrAppUser, IsAppUser, \
|
||||
IsSuperUserOrAppUserOrUserReadonly
|
||||
from .backends import get_command_store, get_multi_command_store, \
|
||||
from .backends import get_command_storage, get_multi_command_storage, \
|
||||
SessionCommandSerializer
|
||||
|
||||
logger = logging.getLogger(__file__)
|
||||
@ -108,7 +109,9 @@ class StatusViewSet(viewsets.ModelViewSet):
|
||||
task_serializer_class = TaskSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
self.handle_sessions()
|
||||
from_gua = self.request.query_params.get("from_guacamole", None)
|
||||
if not from_gua:
|
||||
self.handle_sessions()
|
||||
super().create(request, *args, **kwargs)
|
||||
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
|
||||
serializer = self.task_serializer_class(tasks, many=True)
|
||||
@ -224,8 +227,8 @@ class CommandViewSet(viewsets.ViewSet):
|
||||
}
|
||||
|
||||
"""
|
||||
command_store = get_command_store()
|
||||
multi_command_storage = get_multi_command_store()
|
||||
command_store = get_command_storage()
|
||||
multi_command_storage = get_multi_command_storage()
|
||||
serializer_class = SessionCommandSerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
|
||||
@ -255,10 +258,35 @@ class SessionReplayViewSet(viewsets.ViewSet):
|
||||
serializer_class = ReplaySerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
session = None
|
||||
upload_to = 'replay' # 仅添加到本地存储中
|
||||
|
||||
def gen_session_path(self):
|
||||
def get_session_path(self, version=2):
|
||||
"""
|
||||
获取session日志的文件路径
|
||||
:param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz
|
||||
:return:
|
||||
"""
|
||||
suffix = '.replay.gz'
|
||||
if version == 1:
|
||||
suffix = '.gz'
|
||||
date = self.session.date_start.strftime('%Y-%m-%d')
|
||||
return os.path.join(date, str(self.session.id) + '.gz')
|
||||
return os.path.join(date, str(self.session.id) + suffix)
|
||||
|
||||
def get_local_path(self, version=2):
|
||||
session_path = self.get_session_path(version=version)
|
||||
if version == 2:
|
||||
local_path = os.path.join(self.upload_to, session_path)
|
||||
else:
|
||||
local_path = session_path
|
||||
return local_path
|
||||
|
||||
def save_to_storage(self, f):
|
||||
local_path = self.get_local_path()
|
||||
try:
|
||||
name = default_storage.save(local_path, f)
|
||||
return name, None
|
||||
except OSError as e:
|
||||
return None, e
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
session_id = kwargs.get('pk')
|
||||
@ -267,91 +295,64 @@ class SessionReplayViewSet(viewsets.ViewSet):
|
||||
|
||||
if serializer.is_valid():
|
||||
file = serializer.validated_data['file']
|
||||
file_path = self.gen_session_path()
|
||||
try:
|
||||
default_storage.save(file_path, file)
|
||||
return Response({'url': default_storage.url(file_path)},
|
||||
status=201)
|
||||
except IOError:
|
||||
return Response("Save error", status=500)
|
||||
name, err = self.save_to_storage(file)
|
||||
if not name:
|
||||
msg = "Failed save replay `{}`: {}".format(session_id, err)
|
||||
logger.error(msg)
|
||||
return Response({'msg': str(err)}, status=400)
|
||||
url = default_storage.url(name)
|
||||
return Response({'url': url}, status=201)
|
||||
else:
|
||||
logger.error(
|
||||
'Update load data invalid: {}'.format(serializer.errors))
|
||||
msg = 'Upload data invalid: {}'.format(serializer.errors)
|
||||
logger.error(msg)
|
||||
return Response({'msg': serializer.errors}, status=401)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
session_id = kwargs.get('pk')
|
||||
self.session = get_object_or_404(Session, id=session_id)
|
||||
path = self.gen_session_path()
|
||||
# 新版本和老版本的文件后缀不同
|
||||
session_path = self.get_session_path() # 存在外部存储上的路径
|
||||
local_path = self.get_local_path()
|
||||
local_path_v1 = self.get_local_path(version=1)
|
||||
|
||||
if default_storage.exists(path):
|
||||
url = default_storage.url(path)
|
||||
return redirect(url)
|
||||
else:
|
||||
configs = settings.TERMINAL_REPLAY_STORAGE.items()
|
||||
if not configs:
|
||||
return HttpResponseNotFound()
|
||||
# 去default storage中查找
|
||||
for _local_path in (local_path, local_path_v1, session_path):
|
||||
if default_storage.exists(_local_path):
|
||||
url = default_storage.url(_local_path)
|
||||
return redirect(url)
|
||||
|
||||
for name, config in configs:
|
||||
client = jms_storage.init(config)
|
||||
date = self.session.date_start.strftime('%Y-%m-%d')
|
||||
file_path = os.path.join(date, str(self.session.id) + '.replay.gz')
|
||||
target_path = default_storage.base_location + '/' + path
|
||||
# 去定义的外部storage查找
|
||||
configs = settings.TERMINAL_REPLAY_STORAGE
|
||||
configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'}
|
||||
if not configs:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if client and client.has_file(file_path) and \
|
||||
client.download_file(file_path, target_path):
|
||||
return redirect(default_storage.url(path))
|
||||
return HttpResponseNotFound()
|
||||
target_path = os.path.join(default_storage.base_location, local_path) # 保存到storage的路径
|
||||
target_dir = os.path.dirname(target_path)
|
||||
if not os.path.isdir(target_dir):
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
storage = jms_storage.get_multi_object_storage(configs)
|
||||
ok, err = storage.download(session_path, target_path)
|
||||
if not ok:
|
||||
logger.error("Failed download replay file: {}".format(err))
|
||||
return HttpResponseNotFound()
|
||||
return redirect(default_storage.url(local_path))
|
||||
|
||||
|
||||
class SessionReplayV2ViewSet(viewsets.ViewSet):
|
||||
class SessionReplayV2ViewSet(SessionReplayViewSet):
|
||||
serializer_class = ReplaySerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
session = None
|
||||
|
||||
def gen_session_path(self):
|
||||
date = self.session.date_start.strftime('%Y-%m-%d')
|
||||
replay = {
|
||||
"id": self.session.id,
|
||||
# "width": 100,
|
||||
# "heith": 100
|
||||
}
|
||||
if self.session.protocol == "ssh":
|
||||
replay['type'] = "json"
|
||||
replay['path'] = os.path.join(date, str(self.session.id) + '.gz')
|
||||
return replay
|
||||
elif self.session.protocol == "rdp":
|
||||
replay['type'] = "mp4"
|
||||
replay['path'] = os.path.join(date, str(self.session.id) + '.mp4')
|
||||
return replay
|
||||
else:
|
||||
return replay
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
session_id = kwargs.get('pk')
|
||||
self.session = get_object_or_404(Session, id=session_id)
|
||||
replay = self.gen_session_path()
|
||||
|
||||
if replay.get("path", "") == "":
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if default_storage.exists(replay["path"]):
|
||||
replay["src"] = default_storage.url(replay["path"])
|
||||
return Response(replay)
|
||||
else:
|
||||
configs = settings.TERMINAL_REPLAY_STORAGE.items()
|
||||
if not configs:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
for name, config in configs:
|
||||
client = jms_storage.init(config)
|
||||
|
||||
target_path = default_storage.base_location + '/' + replay["path"]
|
||||
|
||||
if client and client.has_file(replay["path"]) and \
|
||||
client.download_file(replay["path"], target_path):
|
||||
replay["src"] = default_storage.url(replay["path"])
|
||||
return Response(replay)
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
data = {
|
||||
'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
|
||||
'src': '',
|
||||
}
|
||||
if isinstance(response, HttpResponseRedirectBase):
|
||||
data['src'] = response.url
|
||||
return Response(data)
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
|
@ -7,19 +7,19 @@ TYPE_ENGINE_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
def get_command_store():
|
||||
params = settings.COMMAND_STORAGE
|
||||
engine_class = import_module(params['ENGINE'])
|
||||
storage = engine_class.CommandStore(params)
|
||||
def get_command_storage():
|
||||
config = settings.COMMAND_STORAGE
|
||||
engine_class = import_module(config['ENGINE'])
|
||||
storage = engine_class.CommandStore(config)
|
||||
return storage
|
||||
|
||||
|
||||
def get_terminal_command_store():
|
||||
def get_terminal_command_storages():
|
||||
storage_list = {}
|
||||
for name, params in settings.TERMINAL_COMMAND_STORAGE.items():
|
||||
tp = params['TYPE']
|
||||
if tp == 'server':
|
||||
storage = get_command_store()
|
||||
storage = get_command_storage()
|
||||
else:
|
||||
if not TYPE_ENGINE_MAPPING.get(tp):
|
||||
continue
|
||||
@ -29,9 +29,9 @@ def get_terminal_command_store():
|
||||
return storage_list
|
||||
|
||||
|
||||
def get_multi_command_store():
|
||||
def get_multi_command_storage():
|
||||
from .command.multi import CommandStore
|
||||
storage_list = get_terminal_command_store().values()
|
||||
storage_list = get_terminal_command_storages().values()
|
||||
storage = CommandStore(storage_list)
|
||||
return storage
|
||||
|
||||
|
@ -1,41 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from jms_es_sdk import ESStore
|
||||
from jms_storage.es import ESStorage
|
||||
from .base import CommandBase
|
||||
from .models import AbstractSessionCommand
|
||||
|
||||
|
||||
class CommandStore(CommandBase, ESStore):
|
||||
class CommandStore(ESStorage, CommandBase):
|
||||
def __init__(self, params):
|
||||
hosts = params.get('HOSTS', ['http://localhost'])
|
||||
ESStore.__init__(self, hosts=hosts)
|
||||
|
||||
def save(self, command):
|
||||
return ESStore.save(self, command)
|
||||
|
||||
def bulk_save(self, commands):
|
||||
return ESStore.bulk_save(self, commands)
|
||||
super().__init__(params)
|
||||
|
||||
def filter(self, date_from=None, date_to=None,
|
||||
user=None, asset=None, system_user=None,
|
||||
input=None, session=None):
|
||||
|
||||
data = ESStore.filter(
|
||||
self, date_from=date_from, date_to=date_to,
|
||||
user=user, asset=asset, system_user=system_user,
|
||||
input=input, session=session
|
||||
)
|
||||
data = super().filter(date_from=date_from, date_to=date_to,
|
||||
user=user, asset=asset, system_user=system_user,
|
||||
input=input, session=session)
|
||||
return AbstractSessionCommand.from_multi_dict(
|
||||
[item["_source"] for item in data["hits"] if item]
|
||||
)
|
||||
|
||||
def count(self, date_from=None, date_to=None,
|
||||
user=None, asset=None, system_user=None,
|
||||
input=None, session=None):
|
||||
amount = ESStore.count(
|
||||
self, date_from=date_from, date_to=date_to,
|
||||
user=user, asset=asset, system_user=system_user,
|
||||
input=input, session=session
|
||||
)
|
||||
return amount
|
||||
|
@ -9,7 +9,7 @@ from rest_framework_bulk.serializers import BulkListSerializer
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from common.utils import get_object_or_none
|
||||
from .models import Terminal, Status, Session, Task
|
||||
from .backends import get_multi_command_store
|
||||
from .backends import get_multi_command_storage
|
||||
|
||||
|
||||
class TerminalSerializer(serializers.ModelSerializer):
|
||||
@ -47,7 +47,7 @@ class TerminalSerializer(serializers.ModelSerializer):
|
||||
|
||||
class SessionSerializer(serializers.ModelSerializer):
|
||||
command_amount = serializers.SerializerMethodField()
|
||||
command_store = get_multi_command_store()
|
||||
command_store = get_multi_command_storage()
|
||||
|
||||
class Meta:
|
||||
model = Session
|
||||
|
@ -88,7 +88,7 @@
|
||||
<td>{% trans 'Replay session' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" onclick="window.open('/luna/replay/{{ object.id }}','luna', 'height=600, width=800, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-primary btn-xs" id="btn_reset_password" style="width: 54px">{% trans 'Go' %}</button>
|
||||
<button type="button" onclick="window.open('/luna/replay/{{ object.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-primary btn-xs" id="btn_reset_password" style="width: 54px">{% trans 'Go' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -73,6 +73,7 @@
|
||||
<th class="text-center">{% trans 'System user' %}</th>
|
||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||
<th class="text-center">{% trans 'Login from' %}</th>
|
||||
<th class="text-center">{% trans 'Command' %}</th>
|
||||
<th class="text-center">{% trans 'Date start' %}</th>
|
||||
{# <th class="text-center">{% trans 'Date last active' %}</th>#}
|
||||
@ -92,6 +93,7 @@
|
||||
<td class="text-center">{{ session.system_user }}</td>
|
||||
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
|
||||
<td class="text-center">{{ session.protocol }}</td>
|
||||
<td class="text-center">{{ session.get_login_from_display }}</td>
|
||||
<td class="text-center">{{ session.id | get_session_command_amount }}</td>
|
||||
|
||||
<td class="text-center">{{ session.date_start }}</td>
|
||||
@ -99,10 +101,14 @@
|
||||
<td class="text-center">{{ session.date_start|time_util_with_seconds:session.date_end }}</td>
|
||||
<td>
|
||||
{% if session.is_finished %}
|
||||
<a onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>
|
||||
<a onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>
|
||||
{% else %}
|
||||
<!--<a onclick="window.open('/luna/monitor/{{ session.id }}','luna', 'height=600, width=800, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-monitor" >{% trans "Monitor" %}</a>-->
|
||||
<a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
|
||||
{% if session.protocol == 'rdp' %}
|
||||
<a class="btn btn-xs btn-danger btn-term" disabled value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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'))
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -48,6 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
|
@ -70,6 +70,7 @@
|
||||
<br>
|
||||
<input type="checkbox" id="acceptTerms">
|
||||
<label for="acceptTerms" style="margin-top:20px">{% trans "I agree with the terms and conditions." %}</label>
|
||||
<p id="noTerms" class="red-fonts" style="visibility: hidden; font-size: 10px; margin-top: 10px;">* {% trans 'Please choose the terms and conditions.' %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_form wizard.form %}
|
||||
@ -99,11 +100,7 @@
|
||||
{% if wizard.steps.prev %}
|
||||
<li><a class="fl_goto" name="wizard_goto_step" data-goto="{{ wizard.steps.prev }}">{% trans "Previous" %}</a></li>
|
||||
{% endif %}
|
||||
{#{% if wizard.steps.next %}#}
|
||||
{#<li><a class="fl_goto" name="wizard_goto_step" data-goto="{{ wizard.steps.next }}">{% trans "Next" %}</a></li>#}
|
||||
{#{% endif %}#}
|
||||
{#<li><a id="fl_submit">{% trans "Submit" %}</a></li>#}
|
||||
{#将原来的下一页-替换为提交;修复 每页都提交,最后才能成功问题#}
|
||||
|
||||
{% if wizard.steps.next %}
|
||||
<li><a id="fl_submit" >{% trans "Next" %}</a></li>
|
||||
{% else %}
|
||||
@ -124,20 +121,26 @@
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$('#id_2-otp_level div').eq(2).css('display', 'none');
|
||||
|
||||
$(document).on('click', ".fl_goto", function(){
|
||||
var $form = $('#fl_form');
|
||||
$('<input />', {'name': 'wizard_goto_step', 'value': $(this).data('goto'), 'type': 'hidden'}).appendTo($form);
|
||||
$form.submit();
|
||||
return false;
|
||||
var $form = $('#fl_form');
|
||||
$('<input />', {'name': 'wizard_goto_step', 'value': $(this).data('goto'), 'type': 'hidden'}).appendTo($form);
|
||||
$form.submit();
|
||||
return false;
|
||||
}).on('click', '#fl_submit', function(){
|
||||
$('#fl_form').submit();
|
||||
return false;
|
||||
var isFinish = $('#fl_submit').html() === "{% trans 'Finish' %}";
|
||||
var noChecked = !$('#acceptTerms').prop('checked');
|
||||
if ( isFinish && noChecked){
|
||||
$('#noTerms').css('visibility', 'visible');
|
||||
}
|
||||
else{
|
||||
$('#fl_form').submit();
|
||||
return false;
|
||||
}
|
||||
}).on('click', '#btn-reset-pubkey', function () {
|
||||
var the_url = '{% url "users:user-pubkey-generate" %}';
|
||||
window.open(the_url, "_blank")
|
||||
})
|
||||
var the_url = '{% url "users:user-pubkey-generate" %}';
|
||||
window.open(the_url, "_blank");
|
||||
$('#fl_form').submit();
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -45,13 +45,17 @@
|
||||
</div>
|
||||
<form class="m-t" role="form" method="post" action="">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
|
||||
{% if block_login %}
|
||||
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
||||
{% elif form.errors %}
|
||||
{% if 'captcha' in form.errors %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% else %}
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
|
||||
</div>
|
||||
|
@ -51,6 +51,9 @@
|
||||
<th class="text-center">{% trans 'UA' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'City' %}</th>
|
||||
<th class="text-center">{% trans 'MFA' %}</th>
|
||||
<th class="text-center">{% trans 'Reason' %}</th>
|
||||
<th class="text-center">{% trans 'Status' %}</th>
|
||||
<th class="text-center">{% trans 'Date' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
@ -65,6 +68,9 @@
|
||||
</td>
|
||||
<td class="text-center">{{ login_log.ip }}</td>
|
||||
<td class="text-center">{{ login_log.city }}</td>
|
||||
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||
<td class="text-center">{{ login_log.datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -11,6 +11,7 @@
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
|
||||
</head>
|
||||
|
||||
@ -49,10 +50,20 @@
|
||||
<p class="red-fonts">{{ errors }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required="">
|
||||
<input type="password" id="id_new_password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required="">
|
||||
{# 密码popover #}
|
||||
<div id="container">
|
||||
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
|
||||
<div class="arrow" style="left: 50%;"></div>
|
||||
<h3 class="popover-title" style="display: none;"></h3>
|
||||
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
|
||||
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
|
||||
<div class="popover-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="password-confirm" placeholder="{% trans 'Password again' %}" required="">
|
||||
<input type="password" id="id_confirm_password" class="form-control" name="password-confirm" placeholder="{% trans 'Password again' %}" required="">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary block full-width m-b">{% trans "Setting" %}</button>
|
||||
|
||||
@ -79,4 +90,33 @@
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码强度校验
|
||||
var el = $('#id_password_rules'),
|
||||
idPassword = $('#id_new_password'),
|
||||
idPopover = $('#popover777'),
|
||||
container = $('#container'),
|
||||
progress = $('#id_progress'),
|
||||
password_check_rules = {{ password_check_rules|safe }},
|
||||
minLength = {{ min_length }},
|
||||
top = 146, left = 170;
|
||||
|
||||
// 初始化popover
|
||||
initPopover(container, progress, idPassword, el, password_check_rules);
|
||||
|
||||
// 监听事件
|
||||
idPassword.on('focus', function () {
|
||||
idPopover.css('top', top);
|
||||
idPopover.css('left', left);
|
||||
idPopover.css('display', 'block');
|
||||
});
|
||||
idPassword.on('blur', function () {
|
||||
idPopover.css('display', 'none');
|
||||
});
|
||||
idPassword.on('keyup', function(){
|
||||
var password = idPassword.val();
|
||||
checkPasswordRules(password, minLength);
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
@ -99,6 +99,10 @@
|
||||
{% endif %}
|
||||
</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Source' %}:</td>
|
||||
<td><b>{{ user_object.get_source_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date expired' %}:</td>
|
||||
<td><b>{{ user_object.date_expired|date:"Y-m-j H:i:s" }}</b></td>
|
||||
@ -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);
|
||||
|
@ -68,7 +68,7 @@ var asset_table;
|
||||
|
||||
function initTable() {
|
||||
if (inited){
|
||||
return
|
||||
return asset_table
|
||||
} else {
|
||||
inited = true;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Role' %}</th>
|
||||
<th class="text-center">{% trans 'User group' %}</th>
|
||||
<th class="text-center">{% trans 'Source' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
@ -58,21 +59,21 @@ function initTable() {
|
||||
ele: $('#user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + escape(cellData) + '</a>';
|
||||
$(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('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.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()
|
||||
};
|
||||
|
@ -7,6 +7,8 @@
|
||||
<link href="{% static "css/plugins/cropper/cropper.min.css" %}" rel="stylesheet">
|
||||
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
|
||||
<style>
|
||||
.crop {
|
||||
@ -50,6 +52,16 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.old_password layout="horizontal" %}
|
||||
{% bootstrap_field form.new_password layout="horizontal" %}
|
||||
{# 密码popover #}
|
||||
<div id="container">
|
||||
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
|
||||
<div class="arrow" style="left: 50%;"></div>
|
||||
<h3 class="popover-title" style="display: none;"></h3>
|
||||
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
|
||||
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
|
||||
<div class="popover-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.confirm_password layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
@ -71,5 +83,34 @@
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/cropper/cropper.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
var el = $('#id_password_rules'),
|
||||
idPassword = $('#id_new_password'),
|
||||
idPopover = $('#popover777'),
|
||||
container = $('#container'),
|
||||
progress = $('#id_progress'),
|
||||
password_check_rules = {{ password_check_rules|safe }},
|
||||
minLength = {{ min_length }},
|
||||
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
|
||||
left = 377;
|
||||
|
||||
// 初始化popover
|
||||
initPopover(container, progress, idPassword, el, password_check_rules);
|
||||
|
||||
// 监听事件
|
||||
idPassword.on('focus', function () {
|
||||
idPopover.css('top', top);
|
||||
idPopover.css('left', left);
|
||||
idPopover.css('display', 'block');
|
||||
});
|
||||
idPassword.on('blur', function () {
|
||||
idPopover.css('display', 'none');
|
||||
});
|
||||
idPassword.on('keyup', function(){
|
||||
var password = idPassword.val();
|
||||
checkPasswordRules(password, minLength);
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -91,8 +91,15 @@
|
||||
{% else %}
|
||||
{% trans 'Disable' %}
|
||||
{% endif %}
|
||||
{% if mfa_setting %}
|
||||
( {% trans 'Administrator Settings force MFA login' %} )
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-navy">{% trans 'Source' %}</td>
|
||||
<td>{{ user.get_source_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-navy">{% trans 'Date joined' %}</td>
|
||||
<td>{{ user.date_joined|date:"Y-m-d H:i:s" }}</td>
|
||||
|
@ -67,7 +67,7 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2 col-lg-2" style="padding-top: 0">{% trans 'Or reset by server' %}</label>
|
||||
<div class=" col-sm-9 col-lg-9 ">
|
||||
<a href="{% url 'users:user-pubkey-generate' %}">{% trans 'Reset' %}</a>
|
||||
<a id="reset_pubkey" href="{% url 'users:user-pubkey-generate' %}">{% trans 'Reset' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-line-dashed"></div>
|
||||
@ -89,5 +89,10 @@
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/cropper/cropper.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
}).on('click', '#reset_pubkey', function () {
|
||||
var message = "{% trans 'The new public key has been set successfully, Please download the corresponding private key.' %}";
|
||||
toastr.success(message)
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user