mirror of https://github.com/jumpserver/jumpserver
合并冲突解决
commit
5d63d2369f
18
README.md
18
README.md
|
@ -19,31 +19,25 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
|
|||
----
|
||||
|
||||
### 功能
|
||||
- 统一认证
|
||||
- 资产管理
|
||||
- 统一授权
|
||||
- 审计
|
||||
- 支持LDAP认证
|
||||
- Web terminal
|
||||
- SSH Server
|
||||
- 支持Windows RDP
|
||||
|
||||
![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver13.jpg "Jumpserver功能")
|
||||
|
||||
### 开始使用
|
||||
|
||||
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/latest/quickstart.html)
|
||||
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
|
||||
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/latest/step_by_step.html)
|
||||
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
|
||||
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
|
||||
|
||||
### Demo 和 截图
|
||||
### Demo 和 截图
|
||||
|
||||
我们提供了DEMO和截图可以让你快速了解Jumpserver
|
||||
|
||||
[DEMO](http://demo.jumpserver.org)
|
||||
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
### SDK
|
||||
|
||||
我们还编写了一些SDK,供你其它系统快速和Jumpserver APi交互,
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(",")
|
||||
|
|
|
@ -173,14 +173,14 @@ function APIUpdateAttr(props) {
|
|||
}
|
||||
if (typeof props.success === 'function') {
|
||||
return props.success(data);
|
||||
}
|
||||
}
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
if (flash_message) {
|
||||
toastr.error(fail_message);
|
||||
}
|
||||
if (typeof props.error === 'function') {
|
||||
return props.error(jqXHR.responseText);
|
||||
}
|
||||
}
|
||||
});
|
||||
// return true;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -219,7 +220,7 @@ function objectDelete(obj, name, url, redirectTo) {
|
|||
confirmButtonText: '确认',
|
||||
closeOnConfirm: true,
|
||||
}, function () {
|
||||
doDelete()
|
||||
doDelete()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -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