mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-07-17 16:31:28 +00:00
commit
722bf786f1
@ -24,9 +24,9 @@ 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)
|
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
__version__ = "1.3.2"
|
__version__ = "1.3.3"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework_bulk import BulkModelViewSet
|
from rest_framework_bulk import BulkModelViewSet
|
||||||
@ -22,7 +24,8 @@ from ..utils import LabelFilter
|
|||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AssetViewSet', 'AssetListUpdateApi',
|
'AssetViewSet', 'AssetListUpdateApi',
|
||||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi'
|
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
|
||||||
|
'AssetGatewayApi'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -106,3 +109,20 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
|||||||
asset = get_object_or_404(Asset, pk=asset_id)
|
asset = get_object_or_404(Asset, pk=asset_id)
|
||||||
task = test_asset_connectability_manual.delay(asset)
|
task = test_asset_connectability_manual.delay(asset)
|
||||||
return Response({"task": task.id})
|
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)
|
@ -16,7 +16,7 @@ class AssetCreateForm(forms.ModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'hostname', 'ip', 'public_ip', 'port', 'comment',
|
'hostname', 'ip', 'public_ip', 'port', 'comment',
|
||||||
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
|
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
|
||||||
'domain',
|
'domain', 'protocol',
|
||||||
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@ -56,7 +56,7 @@ class AssetUpdateForm(forms.ModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
|
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
|
||||||
'public_ip', 'number', 'comment', 'admin_user', 'labels',
|
'public_ip', 'number', 'comment', 'admin_user', 'labels',
|
||||||
'domain',
|
'domain', 'protocol',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'nodes': forms.SelectMultiple(attrs={
|
'nodes': forms.SelectMultiple(attrs={
|
||||||
|
@ -93,14 +93,21 @@ class SystemUserForm(PasswordAndKeyAuthForm):
|
|||||||
# Because we define custom field, so we need rewrite :method: `save`
|
# Because we define custom field, so we need rewrite :method: `save`
|
||||||
system_user = super().save()
|
system_user = super().save()
|
||||||
password = self.cleaned_data.get('password', '') or None
|
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)
|
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
|
||||||
private_key, public_key = super().gen_keys()
|
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:
|
if auto_generate_key:
|
||||||
logger.info('Auto generate key and set system user auth')
|
logger.info('Auto generate key and set system user auth')
|
||||||
system_user.auto_gen_auth()
|
system_user.auto_gen_auth()
|
||||||
else:
|
else:
|
||||||
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
|
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
|
||||||
|
|
||||||
return system_user
|
return system_user
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -109,12 +116,24 @@ class SystemUserForm(PasswordAndKeyAuthForm):
|
|||||||
if not self.instance and not auto_generate:
|
if not self.instance and not auto_generate:
|
||||||
super().validate_password_key()
|
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:
|
class Meta:
|
||||||
model = SystemUser
|
model = SystemUser
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'username', 'protocol', 'auto_generate_key',
|
'name', 'username', 'protocol', 'auto_generate_key',
|
||||||
'password', 'private_key_file', 'auto_push', 'sudo',
|
'password', 'private_key_file', 'auto_push', 'sudo',
|
||||||
'comment', 'shell', 'priority',
|
'comment', 'shell', 'priority', 'login_mode',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||||
@ -124,5 +143,8 @@ class SystemUserForm(PasswordAndKeyAuthForm):
|
|||||||
'name': '* required',
|
'name': '* required',
|
||||||
'username': '* required',
|
'username': '* required',
|
||||||
'auto_push': _('Auto push system user to asset'),
|
'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.')
|
||||||
}
|
}
|
@ -57,13 +57,27 @@ class Asset(models.Model):
|
|||||||
('MacOS', 'MacOS'),
|
('MacOS', 'MacOS'),
|
||||||
('BSD', 'BSD'),
|
('BSD', 'BSD'),
|
||||||
('Windows', 'Windows'),
|
('Windows', 'Windows'),
|
||||||
|
('Windows2016', 'Windows(2016)'),
|
||||||
('Other', 'Other'),
|
('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)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'),
|
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'),
|
||||||
db_index=True)
|
db_index=True)
|
||||||
hostname = models.CharField(max_length=128, unique=True,
|
hostname = models.CharField(max_length=128, unique=True,
|
||||||
verbose_name=_('Hostname'))
|
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'))
|
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||||
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES,
|
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES,
|
||||||
default='Linux', verbose_name=_('Platform'))
|
default='Linux', verbose_name=_('Platform'))
|
||||||
|
@ -19,7 +19,7 @@ signer = get_signer()
|
|||||||
class AssetUser(models.Model):
|
class AssetUser(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||||
username = models.CharField(max_length=32, verbose_name=_('Username'), validators=[alphanumeric])
|
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'))
|
_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, ])
|
_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'))
|
_public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
||||||
|
@ -95,9 +95,18 @@ class AdminUser(AssetUser):
|
|||||||
class SystemUser(AssetUser):
|
class SystemUser(AssetUser):
|
||||||
SSH_PROTOCOL = 'ssh'
|
SSH_PROTOCOL = 'ssh'
|
||||||
RDP_PROTOCOL = 'rdp'
|
RDP_PROTOCOL = 'rdp'
|
||||||
|
TELNET_PROTOCOL = 'telnet'
|
||||||
PROTOCOL_CHOICES = (
|
PROTOCOL_CHOICES = (
|
||||||
(SSH_PROTOCOL, 'ssh'),
|
(SSH_PROTOCOL, 'ssh'),
|
||||||
(RDP_PROTOCOL, 'rdp'),
|
(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"))
|
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'))
|
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
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):
|
def __str__(self):
|
||||||
return '{0.name}({0.username})'.format(self)
|
return '{0.name}({0.username})'.format(self)
|
||||||
|
@ -43,7 +43,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
|
|||||||
fields = (
|
fields = (
|
||||||
"id", "hostname", "ip", "port", "system_users_granted",
|
"id", "hostname", "ip", "port", "system_users_granted",
|
||||||
"is_active", "system_users_join", "os", 'domain',
|
"is_active", "system_users_join", "os", 'domain',
|
||||||
"platform", "comment"
|
"platform", "comment", "protocol",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -18,6 +18,13 @@ class SystemUserSerializer(serializers.ModelSerializer):
|
|||||||
model = SystemUser
|
model = SystemUser
|
||||||
exclude = ('_password', '_private_key', '_public_key')
|
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
|
@staticmethod
|
||||||
def get_unreachable_assets(obj):
|
def get_unreachable_assets(obj):
|
||||||
return obj.unreachable_assets
|
return obj.unreachable_assets
|
||||||
@ -46,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer):
|
|||||||
model = SystemUser
|
model = SystemUser
|
||||||
fields = [
|
fields = [
|
||||||
"id", "name", "username", "protocol",
|
"id", "name", "username", "protocol",
|
||||||
"password", "private_key",
|
"login_mode", "password", "private_key",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -56,7 +63,10 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SystemUser
|
model = SystemUser
|
||||||
fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',)
|
fields = (
|
||||||
|
'id', 'name', 'username', 'priority',
|
||||||
|
'protocol', 'comment', 'login_mode'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||||
|
@ -36,12 +36,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<h3>{% trans 'Basic' %}</h3>
|
<h3>{% trans 'Basic' %}</h3>
|
||||||
{% bootstrap_field form.name layout="horizontal" %}
|
{% bootstrap_field form.name layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.login_mode layout="horizontal" %}
|
||||||
{% bootstrap_field form.username layout="horizontal" %}
|
{% bootstrap_field form.username layout="horizontal" %}
|
||||||
{% bootstrap_field form.priority layout="horizontal" %}
|
{% bootstrap_field form.priority layout="horizontal" %}
|
||||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||||
|
|
||||||
|
<h3 id="auth_title_id">{% trans 'Auth' %}</h3>
|
||||||
{% block auth %}
|
{% block auth %}
|
||||||
<h3>{% trans 'Auth' %}</h3>
|
|
||||||
<div class="auto-generate">
|
<div class="auto-generate">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||||
@ -80,15 +81,22 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block custom_foot_js %}
|
{% block custom_foot_js %}
|
||||||
<script>
|
<script>
|
||||||
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
|
|
||||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||||
|
var login_mode_id = '#' + '{{ form.login_mode.id_for_label }}';
|
||||||
|
|
||||||
|
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 private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||||
var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
|
var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
|
||||||
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
||||||
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
||||||
|
|
||||||
var need_change_field = [
|
var need_change_field = [
|
||||||
auto_generate_key, private_key_id, auto_push_id, sudo_id, shell_id
|
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() {
|
function protocolChange() {
|
||||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||||
@ -96,7 +104,19 @@ function protocolChange() {
|
|||||||
$.each(need_change_field, function (index, value) {
|
$.each(need_change_field, function (index, value) {
|
||||||
$(value).closest('.form-group').addClass('hidden')
|
$(value).closest('.form-group').addClass('hidden')
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
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();
|
authFieldsDisplay();
|
||||||
$.each(need_change_field, function (index, value) {
|
$.each(need_change_field, function (index, value) {
|
||||||
$(value).closest('.form-group').removeClass('hidden')
|
$(value).closest('.form-group').removeClass('hidden')
|
||||||
@ -111,18 +131,35 @@ function authFieldsDisplay() {
|
|||||||
$('.auth-fields').removeClass('hidden');
|
$('.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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if($(login_mode_id).val() === 'auto'){
|
||||||
|
$('#auth_title_id').removeClass('hidden');
|
||||||
|
$(password_id).closest('.form-group').removeClass('hidden')
|
||||||
|
protocolChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('.select2').select2();
|
$('.select2').select2();
|
||||||
authFieldsDisplay();
|
authFieldsDisplay();
|
||||||
protocolChange();
|
protocolChange();
|
||||||
|
loginModeChange();
|
||||||
})
|
})
|
||||||
.on('change', protocol_id, function(){
|
.on('change', protocol_id, function(){
|
||||||
protocolChange();
|
protocolChange();
|
||||||
})
|
})
|
||||||
.on('change', auto_generate_key, function(){
|
.on('change', auto_generate_key, function(){
|
||||||
authFieldsDisplay();
|
authFieldsDisplay();
|
||||||
});
|
})
|
||||||
|
.on('change', login_mode_id, function(){
|
||||||
|
loginModeChange();
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block help_message %}
|
{% block help_message %}
|
||||||
<div class="alert alert-info help-message">
|
<div class="alert alert-info help-message">
|
||||||
管理用户是服务器的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||||
Windows或其它硬件可以随意设置一个
|
Windows或其它硬件可以随意设置一个
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -107,6 +107,3 @@ $(document).ready(function(){
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
{% bootstrap_field form.hostname layout="horizontal" %}
|
{% bootstrap_field form.hostname layout="horizontal" %}
|
||||||
{% bootstrap_field form.platform layout="horizontal" %}
|
{% bootstrap_field form.platform layout="horizontal" %}
|
||||||
{% bootstrap_field form.ip layout="horizontal" %}
|
{% bootstrap_field form.ip layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||||
{% bootstrap_field form.port layout="horizontal" %}
|
{% bootstrap_field form.port layout="horizontal" %}
|
||||||
{% bootstrap_field form.public_ip layout="horizontal" %}
|
{% bootstrap_field form.public_ip layout="horizontal" %}
|
||||||
{% bootstrap_field form.domain layout="horizontal" %}
|
{% bootstrap_field form.domain layout="horizontal" %}
|
||||||
@ -85,14 +86,14 @@ $(document).ready(function () {
|
|||||||
allowClear: true,
|
allowClear: true,
|
||||||
templateSelection: format
|
templateSelection: format
|
||||||
});
|
});
|
||||||
$("#id_platform").change(function (){
|
$("#id_protocol").change(function (){
|
||||||
var platform = $("#id_platform option:selected").text();
|
var protocol = $("#id_protocol option:selected").text();
|
||||||
var port = 22;
|
var port = 22;
|
||||||
if(platform === 'Windows'){
|
if(protocol === 'rdp'){
|
||||||
port = 3389;
|
port = 3389;
|
||||||
}
|
}
|
||||||
if(platform === 'Other'){
|
if(protocol === 'telnet (beta)'){
|
||||||
port = null;
|
port = 23;
|
||||||
}
|
}
|
||||||
$("#id_port").val(port);
|
$("#id_port").val(port);
|
||||||
});
|
});
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
<h3>{% trans 'Basic' %}</h3>
|
<h3>{% trans 'Basic' %}</h3>
|
||||||
{% bootstrap_field form.hostname layout="horizontal" %}
|
{% bootstrap_field form.hostname layout="horizontal" %}
|
||||||
{% bootstrap_field form.ip layout="horizontal" %}
|
{% bootstrap_field form.ip layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||||
{% bootstrap_field form.port layout="horizontal" %}
|
{% bootstrap_field form.port layout="horizontal" %}
|
||||||
{% bootstrap_field form.platform layout="horizontal" %}
|
{% bootstrap_field form.platform layout="horizontal" %}
|
||||||
{% bootstrap_field form.public_ip layout="horizontal" %}
|
{% bootstrap_field form.public_ip layout="horizontal" %}
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
{% extends '_base_list.html' %}
|
{% extends '_base_list.html' %}
|
||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
{% block table_search %}{% endblock %}
|
{% block table_search %}{% endblock %}
|
||||||
|
|
||||||
|
{% block help_message %}
|
||||||
|
<div class="alert alert-info help-message">
|
||||||
|
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登
|
||||||
|
录。
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block table_container %}
|
{% block table_container %}
|
||||||
<div class="uc pull-left m-r-5">
|
<div class="uc pull-left m-r-5">
|
||||||
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
|
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
|
||||||
@ -69,6 +77,3 @@ $(document).ready(function(){
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
{% bootstrap_field form.domain layout="horizontal" %}
|
{% bootstrap_field form.domain layout="horizontal" %}
|
||||||
|
|
||||||
{% block auth %}
|
{% block auth %}
|
||||||
<h3>{% trans 'Auth' %}</h3>
|
<h3 id="auth_title">{% trans 'Auth' %}</h3>
|
||||||
<div class="auth-fields">
|
<div class="auth-fields">
|
||||||
{% bootstrap_field form.username layout="horizontal" %}
|
{% bootstrap_field form.username layout="horizontal" %}
|
||||||
{% bootstrap_field form.password layout="horizontal" %}
|
{% bootstrap_field form.password layout="horizontal" %}
|
||||||
@ -72,14 +72,23 @@
|
|||||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||||
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||||
var port = '#' + '{{ form.port.id_for_label }}';
|
var port = '#' + '{{ form.port.id_for_label }}';
|
||||||
|
var username = '#' + '{{ form.username.id_for_label }}';
|
||||||
|
var password = '#' + '{{ form.password.id_for_label }}';
|
||||||
|
var auth_title = '#auth_title';
|
||||||
|
|
||||||
function protocolChange() {
|
function protocolChange() {
|
||||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||||
$(port).val(3389);
|
{#$(port).val(3389);#}
|
||||||
$(private_key_id).closest('.form-group').addClass('hidden')
|
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||||
|
$(username).closest('.form-group').addClass('hidden');
|
||||||
|
$(password).closest('.form-group').addClass('hidden');
|
||||||
|
$(auth_title).addClass('hidden');
|
||||||
} else {
|
} else {
|
||||||
$(port).val(22);
|
{#$(port).val(22);#}
|
||||||
$(private_key_id).closest('.form-group').removeClass('hidden')
|
$(private_key_id).closest('.form-group').removeClass('hidden');
|
||||||
|
$(username).closest('.form-group').removeClass('hidden');
|
||||||
|
$(password).closest('.form-group').removeClass('hidden');
|
||||||
|
$(auth_title).removeClass('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,10 @@
|
|||||||
<td>{% trans 'Username' %}:</td>
|
<td>{% trans 'Username' %}:</td>
|
||||||
<td><b>{{ system_user.username }}</b></td>
|
<td><b>{{ system_user.username }}</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Login mode' %}:</td>
|
||||||
|
<td><b>{{ system_user.get_login_mode_display }}</b></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans 'Protocol' %}:</td>
|
<td>{% trans 'Protocol' %}:</td>
|
||||||
<td><b id="id_protocol_type">{{ system_user.protocol }}</b></td>
|
<td><b id="id_protocol_type">{{ system_user.protocol }}</b></td>
|
||||||
@ -148,15 +152,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{# <tr>#}
|
||||||
<tr>
|
{# <td width="50%">{% trans 'Clear auth' %}:</td>#}
|
||||||
<td width="50%">{% trans 'Clear auth' %}:</td>
|
{# <td>#}
|
||||||
<td>
|
{# <span style="float: right">#}
|
||||||
<span style="float: right">
|
{# <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>#}
|
||||||
<button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>
|
{# </span>#}
|
||||||
</span>
|
{# </td>#}
|
||||||
</td>
|
{# </tr>#}
|
||||||
</tr>
|
|
||||||
|
|
||||||
{# <tr>#}
|
{# <tr>#}
|
||||||
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
|
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
|
||||||
@ -333,11 +336,23 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
}).on('click', '.btn-clear-auth', function () {
|
}).on('click', '.btn-clear-auth', function () {
|
||||||
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
|
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({
|
APIUpdateAttr({
|
||||||
url: the_url,
|
url: the_url,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}"
|
success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}"
|
||||||
});
|
});
|
||||||
|
});
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
<th class="text-center">{% trans 'Name' %}</th>
|
<th class="text-center">{% trans 'Name' %}</th>
|
||||||
<th class="text-center">{% trans 'Username' %}</th>
|
<th class="text-center">{% trans 'Username' %}</th>
|
||||||
<th class="text-center">{% trans 'Protocol' %}</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 'Asset' %}</th>
|
||||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||||
<th class="text-center">{% trans 'Unreachable' %}</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>';
|
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||||
}},
|
}},
|
||||||
{targets: 5, createdCell: function (td, cellData) {
|
{targets: 6, createdCell: function (td, cellData) {
|
||||||
var innerHtml = "";
|
var innerHtml = "";
|
||||||
if (cellData !== 0) {
|
if (cellData !== 0) {
|
||||||
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
|
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>');
|
$(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 = "";
|
var innerHtml = "";
|
||||||
if (cellData !== 0) {
|
if (cellData !== 0) {
|
||||||
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
|
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>');
|
$(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 val = 0;
|
||||||
var innerHtml = "";
|
var innerHtml = "";
|
||||||
var total = rowData.assets_amount;
|
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>');
|
$(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 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);
|
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)
|
$(td).html(update_btn + del_btn)
|
||||||
}}],
|
}}],
|
||||||
ajax_url: '{% url "api-assets:system-user-list" %}',
|
ajax_url: '{% url "api-assets:system-user-list" %}',
|
||||||
columns: [
|
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" }
|
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
|
||||||
],
|
],
|
||||||
op_html: $('#actions').html()
|
op_html: $('#actions').html()
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
{% block auth %}
|
{% block auth %}
|
||||||
<h3>{% trans 'Auth' %}</h3>
|
|
||||||
{% bootstrap_field form.password layout="horizontal" %}
|
{% bootstrap_field form.password layout="horizontal" %}
|
||||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -23,6 +23,8 @@ urlpatterns = [
|
|||||||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||||
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/alive/$',
|
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/alive/$',
|
||||||
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
|
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
|
||||||
|
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/$',
|
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$',
|
||||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$',
|
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$',
|
||||||
|
@ -50,4 +50,3 @@ urlpatterns = [
|
|||||||
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
|
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
|
||||||
url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
|
url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -54,7 +54,8 @@ def test_gateway_connectability(gateway):
|
|||||||
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proxy.connect(gateway.ip, username=gateway.username,
|
proxy.connect(gateway.ip, gateway.port,
|
||||||
|
username=gateway.username,
|
||||||
password=gateway.password,
|
password=gateway.password,
|
||||||
pkey=gateway.private_key_obj)
|
pkey=gateway.private_key_obj)
|
||||||
except(paramiko.AuthenticationException,
|
except(paramiko.AuthenticationException,
|
||||||
|
@ -140,11 +140,6 @@ class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView):
|
|||||||
domain = self.object.domain
|
domain = self.object.domain
|
||||||
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = {
|
context = {
|
||||||
'app': _('Assets'),
|
'app': _('Assets'),
|
||||||
|
@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
|
|||||||
|
|
||||||
|
|
||||||
class SecuritySettingForm(BaseForm):
|
class SecuritySettingForm(BaseForm):
|
||||||
# MFA全局设置
|
# MFA global setting
|
||||||
SECURITY_MFA_AUTH = forms.BooleanField(
|
SECURITY_MFA_AUTH = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("MFA Secondary certification"),
|
label=_("MFA Secondary certification"),
|
||||||
@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
|
|||||||
'authentication (valid for all users, including administrators)'
|
'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(
|
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
||||||
initial=6, label=_("Password minimum length"),
|
initial=6, label=_("Password minimum length"),
|
||||||
min_value=6
|
min_value=6
|
||||||
)
|
)
|
||||||
# 大写字母
|
# upper case
|
||||||
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
||||||
|
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
|
|||||||
'After opening, the user password changes '
|
'After opening, the user password changes '
|
||||||
'and resets must contain uppercase letters')
|
'and resets must contain uppercase letters')
|
||||||
)
|
)
|
||||||
# 小写字母
|
# lower case
|
||||||
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain lowercase letters"),
|
label=_("Must contain lowercase letters"),
|
||||||
help_text=_('After opening, the user password changes '
|
help_text=_('After opening, the user password changes '
|
||||||
'and resets must contain lowercase letters')
|
'and resets must contain lowercase letters')
|
||||||
)
|
)
|
||||||
# 数字
|
# number
|
||||||
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain numeric characters"),
|
label=_("Must contain numeric characters"),
|
||||||
help_text=_('After opening, the user password changes '
|
help_text=_('After opening, the user password changes '
|
||||||
'and resets must contain numeric characters')
|
'and resets must contain numeric characters')
|
||||||
)
|
)
|
||||||
# 特殊字符
|
# special char
|
||||||
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain special characters"),
|
label=_("Must contain special characters"),
|
||||||
|
@ -39,9 +39,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<h3>{% trans "MFA setting" %}</h3>
|
<h3>{% trans "User login settings" %}</h3>
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{% if forloop.counter == 2 %}
|
{% if forloop.counter == 4 %}
|
||||||
<div class="hr-line-dashed"></div>
|
<div class="hr-line-dashed"></div>
|
||||||
<h3>{% trans "Password check rule" %}</h3>
|
<h3>{% trans "Password check rule" %}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -343,10 +343,11 @@ if AUTH_LDAP:
|
|||||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
|
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
|
||||||
|
|
||||||
# Celery using redis as broker
|
# 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 '',
|
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
||||||
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
||||||
'port': CONFIG.REDIS_PORT or 6379,
|
'port': CONFIG.REDIS_PORT or 6379,
|
||||||
|
'db':CONFIG.REDIS_DB_CELERY_BROKER or 3,
|
||||||
}
|
}
|
||||||
CELERY_TASK_SERIALIZER = 'pickle'
|
CELERY_TASK_SERIALIZER = 'pickle'
|
||||||
CELERY_RESULT_SERIALIZER = 'pickle'
|
CELERY_RESULT_SERIALIZER = 'pickle'
|
||||||
@ -367,10 +368,11 @@ CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
|||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'redis_cache.RedisCache',
|
'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 '',
|
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
||||||
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
||||||
'port': CONFIG.REDIS_PORT or 6379,
|
'port': CONFIG.REDIS_PORT or 6379,
|
||||||
|
'db':CONFIG.REDIS_DB_CACHE or 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
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
|
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
||||||
BOOTSTRAP3 = {
|
BOOTSTRAP3 = {
|
||||||
|
@ -93,7 +93,7 @@ class JMSInventory(BaseInventory):
|
|||||||
|
|
||||||
if gateway.password:
|
if gateway.password:
|
||||||
proxy_command_list.insert(
|
proxy_command_list.insert(
|
||||||
0, "sshpass -p {}".format(gateway.password)
|
0, "sshpass -p '{}'".format(gateway.password)
|
||||||
)
|
)
|
||||||
if gateway.private_key:
|
if gateway.private_key:
|
||||||
proxy_command_list.append("-i {}".format(gateway.private_key_file))
|
proxy_command_list.append("-i {}".format(gateway.private_key_file))
|
||||||
|
@ -77,9 +77,9 @@ class UserGrantedAssetsApi(ListAPIView):
|
|||||||
util = AssetPermissionUtil(user)
|
util = AssetPermissionUtil(user)
|
||||||
for k, v in util.get_assets().items():
|
for k, v in util.get_assets().items():
|
||||||
if k.is_unixlike():
|
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:
|
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
|
k.system_users_granted = system_users_granted
|
||||||
queryset.append(k)
|
queryset.append(k)
|
||||||
return queryset
|
return queryset
|
||||||
@ -128,9 +128,9 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
|
|||||||
assets = _assets.keys()
|
assets = _assets.keys()
|
||||||
for k, v in _assets.items():
|
for k, v in _assets.items():
|
||||||
if k.is_unixlike():
|
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:
|
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
|
k.system_users_granted = system_users_granted
|
||||||
node.assets_granted = assets
|
node.assets_granted = assets
|
||||||
queryset.append(node)
|
queryset.append(node)
|
||||||
|
@ -6,13 +6,11 @@ from .. import views
|
|||||||
app_name = 'perms'
|
app_name = 'perms'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^asset-permission$', views.AssetPermissionListView.as_view(), name='asset-permission-list'),
|
url(r'^asset-permission/$', views.AssetPermissionListView.as_view(), name='asset-permission-list'),
|
||||||
url(r'^asset-permission/create$', views.AssetPermissionCreateView.as_view(), name='asset-permission-create'),
|
url(r'^asset-permission/create/$', views.AssetPermissionCreateView.as_view(), name='asset-permission-create'),
|
||||||
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/update$', views.AssetPermissionUpdateView.as_view(), name='asset-permission-update'),
|
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.AssetPermissionUpdateView.as_view(), name='asset-permission-update'),
|
||||||
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})$', views.AssetPermissionDetailView.as_view(),name='asset-permission-detail'),
|
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AssetPermissionDetailView.as_view(),name='asset-permission-detail'),
|
||||||
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/delete$', views.AssetPermissionDeleteView.as_view(), name='asset-permission-delete'),
|
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.AssetPermissionDeleteView.as_view(), name='asset-permission-delete'),
|
||||||
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/user$', views.AssetPermissionUserView.as_view(), name='asset-permission-user-list'),
|
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/user/$', views.AssetPermissionUserView.as_view(), name='asset-permission-user-list'),
|
||||||
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/asset$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'),
|
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/asset/$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,7 +198,8 @@ function objectDelete(obj, name, url, redirectTo) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
var fail = function() {
|
var fail = function() {
|
||||||
swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
||||||
|
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error");
|
||||||
};
|
};
|
||||||
APIUpdateAttr({
|
APIUpdateAttr({
|
||||||
url: url,
|
url: url,
|
||||||
@ -272,7 +273,7 @@ jumpserver.initDataTable = function (options) {
|
|||||||
$(td).html('<input type="checkbox" class="text-center ipt_check" id=99991937>'.replace('99991937', cellData));
|
$(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;
|
columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs;
|
||||||
var select = {
|
var select = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="footer fixed">
|
<div class="footer fixed">
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
Version <strong>1.3.2-{% include '_build.html' %}</strong> GPLv2.
|
Version <strong>1.3.3-{% include '_build.html' %}</strong> GPLv2.
|
||||||
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
|
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -259,10 +259,35 @@ class SessionReplayViewSet(viewsets.ViewSet):
|
|||||||
serializer_class = ReplaySerializer
|
serializer_class = ReplaySerializer
|
||||||
permission_classes = (IsSuperUserOrAppUser,)
|
permission_classes = (IsSuperUserOrAppUser,)
|
||||||
session = None
|
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')
|
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):
|
def create(self, request, *args, **kwargs):
|
||||||
session_id = kwargs.get('pk')
|
session_id = kwargs.get('pk')
|
||||||
@ -271,46 +296,49 @@ class SessionReplayViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
file = serializer.validated_data['file']
|
file = serializer.validated_data['file']
|
||||||
file_path = self.gen_session_path()
|
name, err = self.save_to_storage(file)
|
||||||
try:
|
if not name:
|
||||||
default_storage.save(file_path, file)
|
msg = "Failed save replay `{}`: {}".format(session_id, err)
|
||||||
return Response({'url': default_storage.url(file_path)},
|
logger.error(msg)
|
||||||
status=201)
|
return Response({'msg': str(err)}, status=400)
|
||||||
except IOError:
|
url = default_storage.url(name)
|
||||||
return Response("Save error", status=500)
|
return Response({'url': url}, status=201)
|
||||||
else:
|
else:
|
||||||
logger.error(
|
msg = 'Upload data invalid: {}'.format(serializer.errors)
|
||||||
'Update load data invalid: {}'.format(serializer.errors))
|
logger.error(msg)
|
||||||
return Response({'msg': serializer.errors}, status=401)
|
return Response({'msg': serializer.errors}, status=401)
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
session_id = kwargs.get('pk')
|
session_id = kwargs.get('pk')
|
||||||
self.session = get_object_or_404(Session, id=session_id)
|
self.session = get_object_or_404(Session, id=session_id)
|
||||||
path = self.gen_session_path()
|
|
||||||
|
|
||||||
if default_storage.exists(path):
|
# 新版本和老版本的文件后缀不同
|
||||||
url = default_storage.url(path)
|
session_path = self.get_session_path() # 存在外部存储上的路径
|
||||||
|
local_path = self.get_local_path()
|
||||||
|
local_path_v1 = self.get_local_path(version=1)
|
||||||
|
|
||||||
|
# 去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)
|
return redirect(url)
|
||||||
else:
|
|
||||||
config = settings.TERMINAL_REPLAY_STORAGE
|
|
||||||
configs = copy.deepcopy(config)
|
|
||||||
for cfg in config:
|
|
||||||
if config[cfg]['TYPE'] == 'server':
|
|
||||||
configs.__delitem__(cfg)
|
|
||||||
|
|
||||||
|
# 去定义的外部storage查找
|
||||||
|
configs = settings.TERMINAL_REPLAY_STORAGE
|
||||||
|
configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'}
|
||||||
if not configs:
|
if not configs:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
date = self.session.date_start.strftime('%Y-%m-%d')
|
target_path = os.path.join(default_storage.base_location, local_path) # 保存到storage的路径
|
||||||
file_path = os.path.join(date, str(self.session.id) + '.replay.gz')
|
target_dir = os.path.dirname(target_path)
|
||||||
target_path = default_storage.base_location + '/' + path
|
if not os.path.isdir(target_dir):
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
storage = jms_storage.get_multi_object_storage(configs)
|
storage = jms_storage.get_multi_object_storage(configs)
|
||||||
ok, err = storage.download(file_path, target_path)
|
ok, err = storage.download(session_path, target_path)
|
||||||
if ok:
|
if not ok:
|
||||||
return redirect(default_storage.url(path))
|
|
||||||
else:
|
|
||||||
logger.error("Failed download replay file: {}".format(err))
|
logger.error("Failed download replay file: {}".format(err))
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
return redirect(default_storage.url(local_path))
|
||||||
|
|
||||||
|
|
||||||
class SessionReplayV2ViewSet(SessionReplayViewSet):
|
class SessionReplayV2ViewSet(SessionReplayViewSet):
|
||||||
|
@ -73,6 +73,7 @@
|
|||||||
<th class="text-center">{% trans 'System user' %}</th>
|
<th class="text-center">{% trans 'System user' %}</th>
|
||||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||||
<th class="text-center">{% trans 'Protocol' %}</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 'Command' %}</th>
|
||||||
<th class="text-center">{% trans 'Date start' %}</th>
|
<th class="text-center">{% trans 'Date start' %}</th>
|
||||||
{# <th class="text-center">{% trans 'Date last active' %}</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.system_user }}</td>
|
||||||
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
|
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
|
||||||
<td class="text-center">{{ session.protocol }}</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.id | get_session_command_amount }}</td>
|
||||||
|
|
||||||
<td class="text-center">{{ session.date_start }}</td>
|
<td class="text-center">{{ session.date_start }}</td>
|
||||||
|
@ -3,6 +3,7 @@ import uuid
|
|||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
@ -14,10 +15,11 @@ from .serializers import UserSerializer, UserGroupSerializer, \
|
|||||||
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
|
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
|
||||||
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
||||||
from .tasks import write_login_log_async
|
from .tasks import write_login_log_async
|
||||||
from .models import User, UserGroup
|
from .models import User, UserGroup, LoginLog
|
||||||
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
||||||
IsSuperUserOrAppUser
|
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.mixins import IDInFilterMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
@ -93,6 +95,22 @@ class UserUpdatePKApi(generics.UpdateAPIView):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
class UserUnblockPKApi(generics.UpdateAPIView):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
permission_classes = (IsSuperUser,)
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||||
|
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
user = self.get_object()
|
||||||
|
username = user.username if user else ''
|
||||||
|
key_limit = self.key_prefix_limit.format(username, '*')
|
||||||
|
key_block = self.key_prefix_block.format(username)
|
||||||
|
cache.delete_pattern(key_limit)
|
||||||
|
cache.delete(key_block)
|
||||||
|
|
||||||
|
|
||||||
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
|
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||||
queryset = UserGroup.objects.all()
|
queryset = UserGroup.objects.all()
|
||||||
serializer_class = UserGroupSerializer
|
serializer_class = UserGroupSerializer
|
||||||
@ -128,16 +146,12 @@ class UserToken(APIView):
|
|||||||
return Response({'error': msg}, status=406)
|
return Response({'error': msg}, status=406)
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(APIView):
|
class UserProfile(generics.RetrieveAPIView):
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsAuthenticated,)
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
def get(self, request):
|
def get_object(self):
|
||||||
# return Response(request.user.to_json())
|
return self.request.user
|
||||||
return Response(self.serializer_class(request.user).data)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
return Response(self.serializer_class(request.user).data)
|
|
||||||
|
|
||||||
|
|
||||||
class UserOtpAuthApi(APIView):
|
class UserOtpAuthApi(APIView):
|
||||||
@ -153,10 +167,23 @@ class UserOtpAuthApi(APIView):
|
|||||||
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
||||||
|
|
||||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
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)
|
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)
|
token = generate_token(request, user)
|
||||||
self.write_login_log(request, user)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'token': token,
|
'token': token,
|
||||||
@ -165,7 +192,7 @@ class UserOtpAuthApi(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def write_login_log(request, user):
|
def write_login_log(request, data):
|
||||||
login_ip = request.data.get('remote_addr', None)
|
login_ip = request.data.get('remote_addr', None)
|
||||||
login_type = request.data.get('login_type', '')
|
login_type = request.data.get('login_type', '')
|
||||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||||
@ -173,25 +200,54 @@ class UserOtpAuthApi(APIView):
|
|||||||
if not login_ip:
|
if not login_ip:
|
||||||
login_ip = get_login_ip(request)
|
login_ip = get_login_ip(request)
|
||||||
|
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
user.username, ip=login_ip,
|
'ip': login_ip,
|
||||||
type=login_type, user_agent=user_agent,
|
'type': login_type,
|
||||||
)
|
'user_agent': user_agent
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
class UserAuthApi(APIView):
|
class UserAuthApi(APIView):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||||
|
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||||
|
|
||||||
def post(self, request):
|
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(username, ip)
|
||||||
|
key_block = self.key_prefix_block.format(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:
|
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, key_block)
|
||||||
return Response({'msg': msg}, status=401)
|
return Response({'msg': msg}, status=401)
|
||||||
|
|
||||||
if not user.otp_enabled:
|
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)
|
token = generate_token(request, user)
|
||||||
self.write_login_log(request, user)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'token': token,
|
'token': token,
|
||||||
@ -208,7 +264,8 @@ class UserAuthApi(APIView):
|
|||||||
'otp_url': reverse('api-users:user-otp-auth'),
|
'otp_url': reverse('api-users:user-otp-auth'),
|
||||||
'seed': seed,
|
'seed': seed,
|
||||||
'user': self.serializer_class(user).data
|
'user': self.serializer_class(user).data
|
||||||
}, status=300)
|
}, status=300
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_user_valid(request):
|
def check_user_valid(request):
|
||||||
@ -222,7 +279,7 @@ class UserAuthApi(APIView):
|
|||||||
return user, msg
|
return user, msg
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def write_login_log(request, user):
|
def write_login_log(request, data):
|
||||||
login_ip = request.data.get('remote_addr', None)
|
login_ip = request.data.get('remote_addr', None)
|
||||||
login_type = request.data.get('login_type', '')
|
login_type = request.data.get('login_type', '')
|
||||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||||
@ -230,10 +287,14 @@ class UserAuthApi(APIView):
|
|||||||
if not login_ip:
|
if not login_ip:
|
||||||
login_ip = get_login_ip(request)
|
login_ip = get_login_ip(request)
|
||||||
|
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
user.username, ip=login_ip,
|
'ip': login_ip,
|
||||||
type=login_type, user_agent=user_agent,
|
'type': login_type,
|
||||||
)
|
'user_agent': user_agent,
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
class UserConnectionTokenApi(APIView):
|
class UserConnectionTokenApi(APIView):
|
||||||
|
@ -41,12 +41,40 @@ class LoginLog(models.Model):
|
|||||||
('W', 'Web'),
|
('W', 'Web'),
|
||||||
('T', 'Terminal'),
|
('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)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
username = models.CharField(max_length=20, verbose_name=_('Username'))
|
username = models.CharField(max_length=20, verbose_name=_('Username'))
|
||||||
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
|
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
|
||||||
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
|
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
|
||||||
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
|
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'))
|
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'))
|
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -45,13 +45,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<form class="m-t" role="form" method="post" action="">
|
<form class="m-t" role="form" method="post" action="">
|
||||||
{% csrf_token %}
|
{% 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 %}
|
{% if 'captcha' in form.errors %}
|
||||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="form-group">
|
<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 %}">
|
<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>
|
</div>
|
||||||
|
@ -51,6 +51,9 @@
|
|||||||
<th class="text-center">{% trans 'UA' %}</th>
|
<th class="text-center">{% trans 'UA' %}</th>
|
||||||
<th class="text-center">{% trans 'IP' %}</th>
|
<th class="text-center">{% trans 'IP' %}</th>
|
||||||
<th class="text-center">{% trans 'City' %}</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>
|
<th class="text-center">{% trans 'Date' %}</th>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -65,6 +68,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ login_log.ip }}</td>
|
<td class="text-center">{{ login_log.ip }}</td>
|
||||||
<td class="text-center">{{ login_log.city }}</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>
|
<td class="text-center">{{ login_log.datetime }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -182,6 +182,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr style="{% if not unblock %}display:none{% endif %}">
|
||||||
|
<td>{% trans 'Unblock user' %}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pull-right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" id="btn-unblock-user" style="width: 54px">{% trans 'Unblock' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -275,7 +283,7 @@ $(document).ready(function() {
|
|||||||
.on('select2:unselect', function(evt) {
|
.on('select2:unselect', function(evt) {
|
||||||
var data = evt.params.data;
|
var data = evt.params.data;
|
||||||
delete jumpserver.nodes_selected[data.id];
|
delete jumpserver.nodes_selected[data.id];
|
||||||
})
|
});
|
||||||
})
|
})
|
||||||
.on('click', '#is_active', function() {
|
.on('click', '#is_active', function() {
|
||||||
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
|
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
|
||||||
@ -293,7 +301,7 @@ $(document).ready(function() {
|
|||||||
.on('click', '#force_enable_otp', function() {
|
.on('click', '#force_enable_otp', function() {
|
||||||
{% if request.user == user_object %}
|
{% if request.user == user_object %}
|
||||||
toastr.error("{% trans 'Goto profile page enable MFA' %}");
|
toastr.error("{% trans 'Goto profile page enable MFA' %}");
|
||||||
return
|
return;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
|
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
|
||||||
@ -421,11 +429,45 @@ $(document).ready(function() {
|
|||||||
APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail});
|
APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail});
|
||||||
}).on('click', '.btn-delete-user', function () {
|
}).on('click', '.btn-delete-user', function () {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
var name = "{{ user.name }}";
|
var name = "{{ user_object.name }}";
|
||||||
var uid = "{{ user.id }}";
|
var uid = "{{ user_object.id }}";
|
||||||
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||||
var redirect_url = "{% url 'users:user-list' %}";
|
var redirect_url = "{% url 'users:user-list' %}";
|
||||||
objectDelete($this, name, the_url, redirect_url);
|
objectDelete($this, name, the_url, redirect_url);
|
||||||
|
}).on('click', '#btn-unblock-user', function () {
|
||||||
|
function doReset() {
|
||||||
|
{#var the_url = '{% url "api-users:user-reset-password" pk=user_object.id %}';#}
|
||||||
|
var the_url = '{% url "api-users:user-unblock" pk=user_object.id %}';
|
||||||
|
var body = {};
|
||||||
|
var success = function() {
|
||||||
|
var msg = "{% trans "Success" %}";
|
||||||
|
{#swal("{% trans 'Unblock user' %}", msg, "success");#}
|
||||||
|
swal({
|
||||||
|
title: "{% trans 'Unblock user' %}",
|
||||||
|
text: msg,
|
||||||
|
type: "success"
|
||||||
|
}, function() {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
APIUpdateAttr({
|
||||||
|
url: the_url,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
}
|
||||||
|
swal({
|
||||||
|
title: "{% trans 'Are you sure?' %}",
|
||||||
|
text: "{% trans "After unlocking the user, the user can log in normally."%}",
|
||||||
|
type: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#DD6B55",
|
||||||
|
confirmButtonText: "{% trans 'Confirm' %}",
|
||||||
|
closeOnConfirm: false
|
||||||
|
}, function() {
|
||||||
|
doReset();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -59,7 +59,7 @@ function initTable() {
|
|||||||
ele: $('#user_list_table'),
|
ele: $('#user_list_table'),
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
{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));
|
$(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id));
|
||||||
}},
|
}},
|
||||||
{targets: 4, createdCell: function (td, cellData) {
|
{targets: 4, createdCell: function (td, cellData) {
|
||||||
|
@ -29,6 +29,8 @@ urlpatterns = [
|
|||||||
api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
||||||
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$',
|
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$',
|
||||||
api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
|
api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
|
||||||
|
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/unblock/$',
|
||||||
|
api.UserUnblockPKApi.as_view(), name='user-unblock'),
|
||||||
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/groups/$',
|
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/groups/$',
|
||||||
api.UserUpdateGroupApi.as_view(), name='user-update-group'),
|
api.UserUpdateGroupApi.as_view(), name='user-update-group'),
|
||||||
url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$',
|
url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$',
|
||||||
|
@ -8,13 +8,13 @@ app_name = 'users'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Login view
|
# Login view
|
||||||
url(r'^login$', views.UserLoginView.as_view(), name='login'),
|
url(r'^login/$', views.UserLoginView.as_view(), name='login'),
|
||||||
url(r'^logout$', views.UserLogoutView.as_view(), name='logout'),
|
url(r'^logout/$', views.UserLogoutView.as_view(), name='logout'),
|
||||||
url(r'^login/otp$', views.UserLoginOtpView.as_view(), name='login-otp'),
|
url(r'^login/otp/$', views.UserLoginOtpView.as_view(), name='login-otp'),
|
||||||
url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'),
|
url(r'^password/forgot/$', views.UserForgotPasswordView.as_view(), name='forgot-password'),
|
||||||
url(r'^password/forgot/sendmail-success$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
|
url(r'^password/forgot/sendmail-success/$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
|
||||||
url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'),
|
url(r'^password/reset/$', views.UserResetPasswordView.as_view(), name='reset-password'),
|
||||||
url(r'^password/reset/success$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
url(r'^password/reset/success/$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||||
|
|
||||||
# Profile
|
# Profile
|
||||||
url(r'^profile/$', views.UserProfileView.as_view(), name='user-profile'),
|
url(r'^profile/$', views.UserProfileView.as_view(), name='user-profile'),
|
||||||
@ -29,23 +29,23 @@ urlpatterns = [
|
|||||||
url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
|
url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
|
||||||
|
|
||||||
# User view
|
# User view
|
||||||
url(r'^user$', views.UserListView.as_view(), name='user-list'),
|
url(r'^user/$', views.UserListView.as_view(), name='user-list'),
|
||||||
url(r'^user/export/', views.UserExportView.as_view(), name='user-export'),
|
url(r'^user/export/$', views.UserExportView.as_view(), name='user-export'),
|
||||||
url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
|
url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||||
url(r'^user/import/$', views.UserBulkImportView.as_view(), name='user-import'),
|
url(r'^user/import/$', views.UserBulkImportView.as_view(), name='user-import'),
|
||||||
url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'),
|
url(r'^user/create/$', views.UserCreateView.as_view(), name='user-create'),
|
||||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/update$', views.UserUpdateView.as_view(), name='user-update'),
|
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.UserUpdateView.as_view(), name='user-update'),
|
||||||
url(r'^user/update$', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
|
url(r'^user/update/$', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
|
||||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})$', views.UserDetailView.as_view(), name='user-detail'),
|
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.UserDetailView.as_view(), name='user-detail'),
|
||||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
|
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
|
||||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history', views.UserDetailView.as_view(), name='user-login-history'),
|
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history/$', views.UserDetailView.as_view(), name='user-login-history'),
|
||||||
|
|
||||||
# User group view
|
# User group view
|
||||||
url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'),
|
url(r'^user-group/$', views.UserGroupListView.as_view(), name='user-group-list'),
|
||||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})$', views.UserGroupDetailView.as_view(), name='user-group-detail'),
|
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.UserGroupDetailView.as_view(), name='user-group-detail'),
|
||||||
url(r'^user-group/create$', views.UserGroupCreateView.as_view(), name='user-group-create'),
|
url(r'^user-group/create/$', views.UserGroupCreateView.as_view(), name='user-group-create'),
|
||||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/update$', views.UserGroupUpdateView.as_view(), name='user-group-update'),
|
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.UserGroupUpdateView.as_view(), name='user-group-update'),
|
||||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/assets', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
||||||
|
|
||||||
# Login log
|
# Login log
|
||||||
url(r'^login-log/$', views.LoginLogListView.as_view(), name='login-log-list'),
|
url(r'^login-log/$', views.LoginLogListView.as_view(), name='login-log-list'),
|
||||||
|
@ -13,7 +13,7 @@ import ipaddress
|
|||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.contrib.auth import authenticate, login as auth_login
|
from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
@ -200,16 +200,15 @@ def get_login_ip(request):
|
|||||||
return login_ip
|
return login_ip
|
||||||
|
|
||||||
|
|
||||||
def write_login_log(username, type='', ip='', user_agent=''):
|
def write_login_log(*args, **kwargs):
|
||||||
|
ip = kwargs.get('ip', '')
|
||||||
if not (ip and validate_ip(ip)):
|
if not (ip and validate_ip(ip)):
|
||||||
ip = ip[:15]
|
ip = ip[:15]
|
||||||
city = "Unknown"
|
city = "Unknown"
|
||||||
else:
|
else:
|
||||||
city = get_ip_city(ip)
|
city = get_ip_city(ip)
|
||||||
LoginLog.objects.create(
|
kwargs.update({'ip': ip, 'city': city})
|
||||||
username=username, type=type,
|
LoginLog.objects.create(**kwargs)
|
||||||
ip=ip, city=city, user_agent=user_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ip_city(ip, timeout=10):
|
def get_ip_city(ip, timeout=10):
|
||||||
@ -332,3 +331,44 @@ def check_password_rules(password):
|
|||||||
|
|
||||||
match_obj = re.match(pattern, password)
|
match_obj = re.match(pattern, password)
|
||||||
return bool(match_obj)
|
return bool(match_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_login_failed_count_to_cache(key_limit, key_block):
|
||||||
|
count = cache.get(key_limit)
|
||||||
|
count = count + 1 if count else 1
|
||||||
|
|
||||||
|
setting_limit_time = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_TIME'
|
||||||
|
).first()
|
||||||
|
limit_time = setting_limit_time.cleaned_value if setting_limit_time \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||||
|
|
||||||
|
setting_limit_count = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||||
|
).first()
|
||||||
|
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||||
|
|
||||||
|
if count >= limit_count:
|
||||||
|
cache.set(key_block, 1, int(limit_time)*60)
|
||||||
|
|
||||||
|
cache.set(key_limit, count, int(limit_time)*60)
|
||||||
|
|
||||||
|
|
||||||
|
def is_block_login(key_limit):
|
||||||
|
count = cache.get(key_limit)
|
||||||
|
|
||||||
|
setting_limit_count = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||||
|
).first()
|
||||||
|
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||||
|
|
||||||
|
if count and count >= limit_count:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_need_unblock(key_block):
|
||||||
|
if not cache.get(key_block):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
@ -25,8 +25,10 @@ from common.utils import get_object_or_none
|
|||||||
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
|
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
|
||||||
from common.models import Setting
|
from common.models import Setting
|
||||||
from ..models import User, LoginLog
|
from ..models import User, LoginLog
|
||||||
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \
|
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
|
||||||
get_user_or_tmp_user, set_tmp_user_to_cache, get_password_check_rules, check_password_rules
|
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
||||||
|
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
|
||||||
|
is_block_login, set_user_login_failed_count_to_cache
|
||||||
from ..tasks import write_login_log_async
|
from ..tasks import write_login_log_async
|
||||||
from .. import forms
|
from .. import forms
|
||||||
|
|
||||||
@ -47,7 +49,9 @@ class UserLoginView(FormView):
|
|||||||
form_class = forms.UserLoginForm
|
form_class = forms.UserLoginForm
|
||||||
form_class_captcha = forms.UserLoginCaptchaForm
|
form_class_captcha = forms.UserLoginCaptchaForm
|
||||||
redirect_field_name = 'next'
|
redirect_field_name = 'next'
|
||||||
key_prefix = "_LOGIN_INVALID_{}"
|
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||||
|
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||||
|
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
@ -57,6 +61,16 @@ class UserLoginView(FormView):
|
|||||||
request.session.set_test_cookie()
|
request.session.set_test_cookie()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# limit login authentication
|
||||||
|
ip = get_login_ip(request)
|
||||||
|
username = self.request.POST.get('username')
|
||||||
|
key_limit = self.key_prefix_limit.format(username, ip)
|
||||||
|
if is_block_login(key_limit):
|
||||||
|
return self.render_to_response(self.get_context_data(block_login=True))
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if not self.request.session.test_cookie_worked():
|
if not self.request.session.test_cookie_worked():
|
||||||
return HttpResponse(_("Please enable cookies and try again."))
|
return HttpResponse(_("Please enable cookies and try again."))
|
||||||
@ -65,8 +79,24 @@ class UserLoginView(FormView):
|
|||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
|
# write login failed log
|
||||||
|
username = form.cleaned_data.get('username')
|
||||||
|
data = {
|
||||||
|
'username': username,
|
||||||
|
'mfa': LoginLog.MFA_UNKNOWN,
|
||||||
|
'reason': LoginLog.REASON_PASSWORD,
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
|
|
||||||
|
# limit user login failed count
|
||||||
ip = get_login_ip(self.request)
|
ip = get_login_ip(self.request)
|
||||||
cache.set(self.key_prefix.format(ip), 1, 3600)
|
key_limit = self.key_prefix_limit.format(username, ip)
|
||||||
|
key_block = self.key_prefix_block.format(username)
|
||||||
|
set_user_login_failed_count_to_cache(key_limit, key_block)
|
||||||
|
|
||||||
|
# show captcha
|
||||||
|
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||||
old_form = form
|
old_form = form
|
||||||
form = self.form_class_captcha(data=form.data)
|
form = self.form_class_captcha(data=form.data)
|
||||||
form._errors = old_form.errors
|
form._errors = old_form.errors
|
||||||
@ -74,7 +104,7 @@ class UserLoginView(FormView):
|
|||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
ip = get_login_ip(self.request)
|
ip = get_login_ip(self.request)
|
||||||
if cache.get(self.key_prefix.format(ip)):
|
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||||
return self.form_class_captcha
|
return self.form_class_captcha
|
||||||
else:
|
else:
|
||||||
return self.form_class
|
return self.form_class
|
||||||
@ -91,7 +121,13 @@ class UserLoginView(FormView):
|
|||||||
elif not user.otp_enabled:
|
elif not user.otp_enabled:
|
||||||
# 0 & T,F
|
# 0 & T,F
|
||||||
auth_login(self.request, user)
|
auth_login(self.request, user)
|
||||||
self.write_login_log()
|
data = {
|
||||||
|
'username': self.request.user.username,
|
||||||
|
'mfa': int(self.request.user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_NOTHING,
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -101,13 +137,16 @@ class UserLoginView(FormView):
|
|||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
def write_login_log(self):
|
def write_login_log(self, data):
|
||||||
login_ip = get_login_ip(self.request)
|
login_ip = get_login_ip(self.request)
|
||||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
self.request.user.username, type='W',
|
'ip': login_ip,
|
||||||
ip=login_ip, user_agent=user_agent
|
'type': 'W',
|
||||||
)
|
'user_agent': user_agent
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
class UserLoginOtpView(FormView):
|
class UserLoginOtpView(FormView):
|
||||||
@ -122,22 +161,38 @@ class UserLoginOtpView(FormView):
|
|||||||
|
|
||||||
if check_otp_code(otp_secret_key, otp_code):
|
if check_otp_code(otp_secret_key, otp_code):
|
||||||
auth_login(self.request, user)
|
auth_login(self.request, user)
|
||||||
self.write_login_log()
|
data = {
|
||||||
|
'username': self.request.user.username,
|
||||||
|
'mfa': int(self.request.user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_NOTHING,
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
else:
|
else:
|
||||||
|
data = {
|
||||||
|
'username': user.username,
|
||||||
|
'mfa': int(user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_MFA,
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
form.add_error('otp_code', _('MFA code invalid'))
|
form.add_error('otp_code', _('MFA code invalid'))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||||
|
|
||||||
def write_login_log(self):
|
def write_login_log(self, data):
|
||||||
login_ip = get_login_ip(self.request)
|
login_ip = get_login_ip(self.request)
|
||||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
self.request.user.username, type='W',
|
'ip': login_ip,
|
||||||
ip=login_ip, user_agent=user_agent
|
'type': 'W',
|
||||||
)
|
'user_agent': user_agent
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(never_cache, name='dispatch')
|
@method_decorator(never_cache, name='dispatch')
|
||||||
|
@ -36,7 +36,9 @@ from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
|
|||||||
from common.models import Setting
|
from common.models import Setting
|
||||||
from .. import forms
|
from .. import forms
|
||||||
from ..models import User, UserGroup
|
from ..models import User, UserGroup
|
||||||
from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user_or_tmp_user, get_password_check_rules, check_password_rules
|
from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, \
|
||||||
|
get_user_or_tmp_user, get_password_check_rules, check_password_rules, \
|
||||||
|
is_need_unblock
|
||||||
from ..signals import post_user_create
|
from ..signals import post_user_create
|
||||||
from ..tasks import write_login_log_async
|
from ..tasks import write_login_log_async
|
||||||
|
|
||||||
@ -168,13 +170,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
|
|||||||
model = User
|
model = User
|
||||||
template_name = 'users/user_detail.html'
|
template_name = 'users/user_detail.html'
|
||||||
context_object_name = "user_object"
|
context_object_name = "user_object"
|
||||||
|
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
user = self.get_object()
|
||||||
|
key_block = self.key_prefix_block.format(user.username)
|
||||||
groups = UserGroup.objects.exclude(id__in=self.object.groups.all())
|
groups = UserGroup.objects.exclude(id__in=self.object.groups.all())
|
||||||
context = {
|
context = {
|
||||||
'app': _('Users'),
|
'app': _('Users'),
|
||||||
'action': _('User detail'),
|
'action': _('User detail'),
|
||||||
'groups': groups
|
'groups': groups,
|
||||||
|
'unblock': is_need_unblock(key_block),
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
@ -21,10 +21,10 @@ class Config:
|
|||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
# Development env open this, when error occur display the full process track, Production disable it
|
# Development env open this, when error occur display the full process track, Production disable it
|
||||||
DEBUG = True
|
DEBUG = os.environ.get("DEBUG") or True
|
||||||
|
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/
|
# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/
|
||||||
LOG_LEVEL = 'DEBUG'
|
LOG_LEVEL = os.environ.get("LOG_LEVEL") or 'DEBUG'
|
||||||
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
||||||
|
|
||||||
# Database setting, Support sqlite3, mysql, postgres ....
|
# Database setting, Support sqlite3, mysql, postgres ....
|
||||||
@ -35,12 +35,12 @@ class Config:
|
|||||||
DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3')
|
DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3')
|
||||||
|
|
||||||
# MySQL or postgres setting like:
|
# MySQL or postgres setting like:
|
||||||
# DB_ENGINE = 'mysql'
|
# DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql'
|
||||||
# DB_HOST = '127.0.0.1'
|
# DB_HOST = os.environ.get("DB_HOST") or '127.0.0.1'
|
||||||
# DB_PORT = 3306
|
# DB_PORT = os.environ.get("DB_PORT") or 3306
|
||||||
# DB_USER = 'root'
|
# DB_USER = os.environ.get("DB_USER") or 'jumpserver'
|
||||||
# DB_PASSWORD = ''
|
# DB_PASSWORD = os.environ.get("DB_PASSWORD") or 'weakPassword'
|
||||||
# DB_NAME = 'jumpserver'
|
# DB_NAME = os.environ.get("DB_NAME") or 'jumpserver'
|
||||||
|
|
||||||
# When Django start it will bind this host and port
|
# When Django start it will bind this host and port
|
||||||
# ./manage.py runserver 127.0.0.1:8080
|
# ./manage.py runserver 127.0.0.1:8080
|
||||||
@ -48,9 +48,11 @@ class Config:
|
|||||||
HTTP_LISTEN_PORT = 8080
|
HTTP_LISTEN_PORT = 8080
|
||||||
|
|
||||||
# Use Redis as broker for celery and web socket
|
# Use Redis as broker for celery and web socket
|
||||||
REDIS_HOST = '127.0.0.1'
|
REDIS_HOST = os.environ.get("REDIS_HOST") or '127.0.0.1'
|
||||||
REDIS_PORT = 6379
|
REDIS_PORT = os.environ.get("REDIS_PORT") or 6379
|
||||||
REDIS_PASSWORD = ''
|
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") or ''
|
||||||
|
REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3
|
||||||
|
REDIS_DB_CACHE = os.environ.get('REDIS_DB') or 4
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
@ -1 +1 @@
|
|||||||
libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite gcc automake
|
libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite gcc automake libkrb5-dev
|
||||||
|
@ -61,7 +61,7 @@ pytz==2018.3
|
|||||||
PyYAML==3.12
|
PyYAML==3.12
|
||||||
redis==2.10.6
|
redis==2.10.6
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
jms-storage==0.0.17
|
jms-storage==0.0.18
|
||||||
s3transfer==0.1.13
|
s3transfer==0.1.13
|
||||||
simplejson==3.13.2
|
simplejson==3.13.2
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
|
@ -4,3 +4,5 @@
|
|||||||
python3 ../apps/manage.py makemigrations
|
python3 ../apps/manage.py makemigrations
|
||||||
|
|
||||||
python3 ../apps/manage.py migrate
|
python3 ../apps/manage.py migrate
|
||||||
|
|
||||||
|
python3 ../apps/manage.py makemigrations --merge
|
||||||
|
Loading…
Reference in New Issue
Block a user