mirror of https://github.com/jumpserver/jumpserver
commit
722bf786f1
10
README.md
10
README.md
|
@ -19,25 +19,25 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
|
|||
----
|
||||
|
||||
### 功能
|
||||
|
||||
|
||||

|
||||
|
||||
### 开始使用
|
||||
|
||||
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/latest/quickstart.html)
|
||||
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
|
||||
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/latest/step_by_step.html)
|
||||
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
|
||||
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
|
||||
|
||||
### Demo 和 截图
|
||||
### Demo 和 截图
|
||||
|
||||
我们提供了DEMO和截图可以让你快速了解Jumpserver
|
||||
|
||||
[DEMO](http://demo.jumpserver.org)
|
||||
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
### SDK
|
||||
|
||||
我们还编写了一些SDK,供你其它系统快速和Jumpserver APi交互,
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__version__ = "1.3.2"
|
||||
__version__ = "1.3.3"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import random
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
@ -22,7 +24,8 @@ from ..utils import LabelFilter
|
|||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet', 'AssetListUpdateApi',
|
||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi'
|
||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
|
||||
'AssetGatewayApi'
|
||||
]
|
||||
|
||||
|
||||
|
@ -106,3 +109,20 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
|||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
task = test_asset_connectability_manual.delay(asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AssetGatewayApi(generics.RetrieveAPIView):
|
||||
queryset = Asset.objects.all()
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
|
||||
if asset.domain and \
|
||||
asset.domain.gateways.filter(protocol=asset.protocol).exists():
|
||||
gateway = random.choice(asset.domain.gateways.filter(protocol=asset.protocol))
|
||||
serializer = serializers.GatewayWithAuthSerializer(instance=gateway)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response({"msg": "Not have gateway"}, status=404)
|
|
@ -16,7 +16,7 @@ class AssetCreateForm(forms.ModelForm):
|
|||
fields = [
|
||||
'hostname', 'ip', 'public_ip', 'port', 'comment',
|
||||
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
|
||||
'domain',
|
||||
'domain', 'protocol',
|
||||
|
||||
]
|
||||
widgets = {
|
||||
|
@ -56,7 +56,7 @@ class AssetUpdateForm(forms.ModelForm):
|
|||
fields = [
|
||||
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
|
||||
'public_ip', 'number', 'comment', 'admin_user', 'labels',
|
||||
'domain',
|
||||
'domain', 'protocol',
|
||||
]
|
||||
widgets = {
|
||||
'nodes': forms.SelectMultiple(attrs={
|
||||
|
|
|
@ -93,14 +93,21 @@ class SystemUserForm(PasswordAndKeyAuthForm):
|
|||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
system_user = super().save()
|
||||
password = self.cleaned_data.get('password', '') or None
|
||||
login_mode = self.cleaned_data.get('login_mode', '') or None
|
||||
protocol = self.cleaned_data.get('protocol') or None
|
||||
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
|
||||
private_key, public_key = super().gen_keys()
|
||||
|
||||
if login_mode == SystemUser.MANUAL_LOGIN or protocol == SystemUser.TELNET_PROTOCOL:
|
||||
system_user.auto_push = 0
|
||||
system_user.save()
|
||||
|
||||
if auto_generate_key:
|
||||
logger.info('Auto generate key and set system user auth')
|
||||
system_user.auto_gen_auth()
|
||||
else:
|
||||
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
|
||||
|
||||
return system_user
|
||||
|
||||
def clean(self):
|
||||
|
@ -109,12 +116,24 @@ class SystemUserForm(PasswordAndKeyAuthForm):
|
|||
if not self.instance and not auto_generate:
|
||||
super().validate_password_key()
|
||||
|
||||
def is_valid(self):
|
||||
validated = super().is_valid()
|
||||
username = self.cleaned_data.get('username')
|
||||
login_mode = self.cleaned_data.get('login_mode')
|
||||
if login_mode == SystemUser.AUTO_LOGIN and not username:
|
||||
self.add_error(
|
||||
"username", _('* Automatic login mode,'
|
||||
' must fill in the username.')
|
||||
)
|
||||
return False
|
||||
return validated
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'name', 'username', 'protocol', 'auto_generate_key',
|
||||
'password', 'private_key_file', 'auto_push', 'sudo',
|
||||
'comment', 'shell', 'priority',
|
||||
'comment', 'shell', 'priority', 'login_mode',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
|
@ -124,5 +143,8 @@ class SystemUserForm(PasswordAndKeyAuthForm):
|
|||
'name': '* required',
|
||||
'username': '* required',
|
||||
'auto_push': _('Auto push system user to asset'),
|
||||
'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'),
|
||||
}
|
||||
'priority': _('High level will be using login asset as default, '
|
||||
'if user was granted more than 2 system user'),
|
||||
'login_mode': _('If you choose manual login mode, you do not '
|
||||
'need to fill in the username and password.')
|
||||
}
|
||||
|
|
|
@ -57,13 +57,27 @@ class Asset(models.Model):
|
|||
('MacOS', 'MacOS'),
|
||||
('BSD', 'BSD'),
|
||||
('Windows', 'Windows'),
|
||||
('Windows2016', 'Windows(2016)'),
|
||||
('Other', 'Other'),
|
||||
)
|
||||
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
TELNET_PROTOCOL = 'telnet'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(TELNET_PROTOCOL, 'telnet (beta)'),
|
||||
)
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'),
|
||||
db_index=True)
|
||||
hostname = models.CharField(max_length=128, unique=True,
|
||||
verbose_name=_('Hostname'))
|
||||
protocol = models.CharField(max_length=128, default=SSH_PROTOCOL,
|
||||
choices=PROTOCOL_CHOICES,
|
||||
verbose_name=_('Protocol'))
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES,
|
||||
default='Linux', verbose_name=_('Platform'))
|
||||
|
|
|
@ -19,7 +19,7 @@ signer = get_signer()
|
|||
class AssetUser(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=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'))
|
||||
_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'))
|
||||
|
|
|
@ -95,9 +95,18 @@ class AdminUser(AssetUser):
|
|||
class SystemUser(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
TELNET_PROTOCOL = 'telnet'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(TELNET_PROTOCOL, 'telnet (beta)'),
|
||||
)
|
||||
|
||||
AUTO_LOGIN = 'auto'
|
||||
MANUAL_LOGIN = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
(AUTO_LOGIN, _('Automatic login')),
|
||||
(MANUAL_LOGIN, _('Manually login'))
|
||||
)
|
||||
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
|
@ -107,6 +116,7 @@ class SystemUser(AssetUser):
|
|||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode'))
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
|
|
|
@ -43,7 +43,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
|
|||
fields = (
|
||||
"id", "hostname", "ip", "port", "system_users_granted",
|
||||
"is_active", "system_users_join", "os", 'domain',
|
||||
"platform", "comment"
|
||||
"platform", "comment", "protocol",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -18,6 +18,13 @@ class SystemUserSerializer(serializers.ModelSerializer):
|
|||
model = SystemUser
|
||||
exclude = ('_password', '_private_key', '_public_key')
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
|
||||
fields.extend([
|
||||
'get_login_mode_display',
|
||||
])
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_assets(obj):
|
||||
return obj.unreachable_assets
|
||||
|
@ -46,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer):
|
|||
model = SystemUser
|
||||
fields = [
|
||||
"id", "name", "username", "protocol",
|
||||
"password", "private_key",
|
||||
"login_mode", "password", "private_key",
|
||||
]
|
||||
|
||||
|
||||
|
@ -56,7 +63,10 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
|
|||
"""
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',)
|
||||
fields = (
|
||||
'id', 'name', 'username', 'priority',
|
||||
'protocol', 'comment', 'login_mode'
|
||||
)
|
||||
|
||||
|
||||
class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -36,12 +36,13 @@
|
|||
{% endif %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.login_mode layout="horizontal" %}
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.priority layout="horizontal" %}
|
||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||
|
||||
<h3 id="auth_title_id">{% trans 'Auth' %}</h3>
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
<div class="auto-generate">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||
|
@ -80,15 +81,22 @@
|
|||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
|
||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var 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 auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
|
||||
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
||||
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
||||
|
||||
var need_change_field = [
|
||||
auto_generate_key, private_key_id, auto_push_id, sudo_id, shell_id
|
||||
];
|
||||
var need_change_field_login_mode = [
|
||||
auto_generate_key, private_key_id, auto_push_id, password_id
|
||||
];
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||
|
@ -96,7 +104,19 @@ function protocolChange() {
|
|||
$.each(need_change_field, function (index, value) {
|
||||
$(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();
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').removeClass('hidden')
|
||||
|
@ -111,18 +131,35 @@ function authFieldsDisplay() {
|
|||
$('.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 () {
|
||||
$('.select2').select2();
|
||||
authFieldsDisplay();
|
||||
protocolChange();
|
||||
loginModeChange();
|
||||
})
|
||||
.on('change', protocol_id, function(){
|
||||
protocolChange();
|
||||
})
|
||||
.on('change', auto_generate_key, function(){
|
||||
authFieldsDisplay();
|
||||
});
|
||||
})
|
||||
.on('change', login_mode_id, function(){
|
||||
loginModeChange();
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
管理用户是服务器的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||
管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||
Windows或其它硬件可以随意设置一个
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -107,6 +107,3 @@ $(document).ready(function(){
|
|||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
{% bootstrap_field form.hostname layout="horizontal" %}
|
||||
{% bootstrap_field form.platform layout="horizontal" %}
|
||||
{% bootstrap_field form.ip layout="horizontal" %}
|
||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||
{% bootstrap_field form.port layout="horizontal" %}
|
||||
{% bootstrap_field form.public_ip layout="horizontal" %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
|
@ -85,14 +86,14 @@ $(document).ready(function () {
|
|||
allowClear: true,
|
||||
templateSelection: format
|
||||
});
|
||||
$("#id_platform").change(function (){
|
||||
var platform = $("#id_platform option:selected").text();
|
||||
$("#id_protocol").change(function (){
|
||||
var protocol = $("#id_protocol option:selected").text();
|
||||
var port = 22;
|
||||
if(platform === 'Windows'){
|
||||
if(protocol === 'rdp'){
|
||||
port = 3389;
|
||||
}
|
||||
if(platform === 'Other'){
|
||||
port = null;
|
||||
if(protocol === 'telnet (beta)'){
|
||||
port = 23;
|
||||
}
|
||||
$("#id_port").val(port);
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.hostname layout="horizontal" %}
|
||||
{% bootstrap_field form.ip layout="horizontal" %}
|
||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||
{% bootstrap_field form.port layout="horizontal" %}
|
||||
{% bootstrap_field form.platform layout="horizontal" %}
|
||||
{% bootstrap_field form.public_ip layout="horizontal" %}
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block table_search %}{% endblock %}
|
||||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登
|
||||
录。
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
|
||||
|
@ -69,6 +77,3 @@ $(document).ready(function(){
|
|||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
<h3 id="auth_title">{% trans 'Auth' %}</h3>
|
||||
<div class="auth-fields">
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
|
@ -72,14 +72,23 @@
|
|||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||
var port = '#' + '{{ form.port.id_for_label }}';
|
||||
var username = '#' + '{{ form.username.id_for_label }}';
|
||||
var password = '#' + '{{ form.password.id_for_label }}';
|
||||
var auth_title = '#auth_title';
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||
$(port).val(3389);
|
||||
$(private_key_id).closest('.form-group').addClass('hidden')
|
||||
{#$(port).val(3389);#}
|
||||
$(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 {
|
||||
$(port).val(22);
|
||||
$(private_key_id).closest('.form-group').removeClass('hidden')
|
||||
{#$(port).val(22);#}
|
||||
$(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><b>{{ system_user.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Login mode' %}:</td>
|
||||
<td><b>{{ system_user.get_login_mode_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Protocol' %}:</td>
|
||||
<td><b id="id_protocol_type">{{ system_user.protocol }}</b></td>
|
||||
|
@ -148,15 +152,14 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Clear auth' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{# <tr>#}
|
||||
{# <td width="50%">{% trans 'Clear auth' %}:</td>#}
|
||||
{# <td>#}
|
||||
{# <span style="float: right">#}
|
||||
{# <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>#}
|
||||
{# </span>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
|
||||
{# <tr>#}
|
||||
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
|
||||
|
@ -333,10 +336,22 @@ $(document).ready(function () {
|
|||
});
|
||||
}).on('click', '.btn-clear-auth', function () {
|
||||
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'DELETE',
|
||||
success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}"
|
||||
var name = '{{ system_user.name }}';
|
||||
swal({
|
||||
title: '你确定清除该系统用户的认证信息吗 ?',
|
||||
text: " [" + name + "] ",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonColor: "#ed5565",
|
||||
confirmButtonText: '确认',
|
||||
closeOnConfirm: true
|
||||
}, function () {
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'DELETE',
|
||||
success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}"
|
||||
});
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||
<th class="text-center">{% trans 'Login mode' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Unreachable' %}</th>
|
||||
|
@ -48,7 +49,7 @@ function initTable() {
|
|||
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
|
||||
|
@ -57,7 +58,7 @@ function initTable() {
|
|||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
{targets: 7, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
|
||||
|
@ -66,7 +67,7 @@ function initTable() {
|
|||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 8, createdCell: function (td, cellData, rowData) {
|
||||
var val = 0;
|
||||
var innerHtml = "";
|
||||
var total = rowData.assets_amount;
|
||||
|
@ -84,14 +85,14 @@ function initTable() {
|
|||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
|
||||
}},
|
||||
{targets: 9, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 10, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:system-user-list" %}',
|
||||
columns: [
|
||||
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "assets_amount" },
|
||||
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" },
|
||||
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
{% load bootstrap3 %}
|
||||
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
|
|
|
@ -23,6 +23,8 @@ urlpatterns = [
|
|||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/alive/$',
|
||||
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
|
||||
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/$',
|
||||
api.AssetGatewayApi.as_view(), name='asset-gateway'),
|
||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$',
|
||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$',
|
||||
|
|
|
@ -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/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())
|
||||
|
||||
try:
|
||||
proxy.connect(gateway.ip, username=gateway.username,
|
||||
proxy.connect(gateway.ip, gateway.port,
|
||||
username=gateway.username,
|
||||
password=gateway.password,
|
||||
pkey=gateway.private_key_obj)
|
||||
except(paramiko.AuthenticationException,
|
||||
|
|
|
@ -140,11 +140,6 @@ class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView):
|
|||
domain = self.object.domain
|
||||
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
print(form.cleaned_data)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
|
|
|
@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
|
|||
|
||||
|
||||
class SecuritySettingForm(BaseForm):
|
||||
# MFA全局设置
|
||||
# MFA global setting
|
||||
SECURITY_MFA_AUTH = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("MFA Secondary certification"),
|
||||
|
@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
|
|||
'authentication (valid for all users, including administrators)'
|
||||
)
|
||||
)
|
||||
# 最小长度
|
||||
# limit login count
|
||||
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
|
||||
initial=3, min_value=3,
|
||||
label=_("Limit the number of login failures")
|
||||
)
|
||||
# limit login time
|
||||
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
|
||||
initial=30, min_value=5,
|
||||
label=_("No logon interval"),
|
||||
help_text=_(
|
||||
"Tip :(unit/minute) if the user has failed to log in for a limited "
|
||||
"number of times, no login is allowed during this time interval."
|
||||
)
|
||||
)
|
||||
# min length
|
||||
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
||||
initial=6, label=_("Password minimum length"),
|
||||
min_value=6
|
||||
)
|
||||
# 大写字母
|
||||
# upper case
|
||||
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
||||
|
||||
initial=False, required=False,
|
||||
|
@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
|
|||
'After opening, the user password changes '
|
||||
'and resets must contain uppercase letters')
|
||||
)
|
||||
# 小写字母
|
||||
# lower case
|
||||
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain lowercase letters"),
|
||||
help_text=_('After opening, the user password changes '
|
||||
'and resets must contain lowercase letters')
|
||||
)
|
||||
# 数字
|
||||
# number
|
||||
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain numeric characters"),
|
||||
help_text=_('After opening, the user password changes '
|
||||
'and resets must contain numeric characters')
|
||||
)
|
||||
# 特殊字符
|
||||
# special char
|
||||
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain special characters"),
|
||||
|
|
|
@ -39,9 +39,9 @@
|
|||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
<h3>{% trans "MFA setting" %}</h3>
|
||||
<h3>{% trans "User login settings" %}</h3>
|
||||
{% for field in form %}
|
||||
{% if forloop.counter == 2 %}
|
||||
{% if forloop.counter == 4 %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans "Password check rule" %}</h3>
|
||||
{% 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)
|
||||
|
||||
# Celery using redis as broker
|
||||
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/3' % {
|
||||
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
||||
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
||||
'port': CONFIG.REDIS_PORT or 6379,
|
||||
'db':CONFIG.REDIS_DB_CELERY_BROKER or 3,
|
||||
}
|
||||
CELERY_TASK_SERIALIZER = 'pickle'
|
||||
CELERY_RESULT_SERIALIZER = 'pickle'
|
||||
|
@ -367,10 +368,11 @@ CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
|||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'redis_cache.RedisCache',
|
||||
'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/4' % {
|
||||
'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
||||
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
||||
'port': CONFIG.REDIS_PORT or 6379,
|
||||
'db':CONFIG.REDIS_DB_CACHE or 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -403,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
|
|||
|
||||
|
||||
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
||||
DEFAULT_LOGIN_LIMIT_COUNT = 3
|
||||
DEFAULT_LOGIN_LIMIT_TIME = 30
|
||||
|
||||
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
||||
BOOTSTRAP3 = {
|
||||
|
|
|
@ -93,7 +93,7 @@ class JMSInventory(BaseInventory):
|
|||
|
||||
if gateway.password:
|
||||
proxy_command_list.insert(
|
||||
0, "sshpass -p {}".format(gateway.password)
|
||||
0, "sshpass -p '{}'".format(gateway.password)
|
||||
)
|
||||
if gateway.private_key:
|
||||
proxy_command_list.append("-i {}".format(gateway.private_key_file))
|
||||
|
|
|
@ -77,9 +77,9 @@ class UserGrantedAssetsApi(ListAPIView):
|
|||
util = AssetPermissionUtil(user)
|
||||
for k, v in util.get_assets().items():
|
||||
if k.is_unixlike():
|
||||
system_users_granted = [s for s in v if s.protocol == 'ssh']
|
||||
system_users_granted = [s for s in v if s.protocol in ['ssh', 'telnet']]
|
||||
else:
|
||||
system_users_granted = [s for s in v if s.protocol == 'rdp']
|
||||
system_users_granted = [s for s in v if s.protocol in ['rdp', 'telnet']]
|
||||
k.system_users_granted = system_users_granted
|
||||
queryset.append(k)
|
||||
return queryset
|
||||
|
@ -128,9 +128,9 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
|
|||
assets = _assets.keys()
|
||||
for k, v in _assets.items():
|
||||
if k.is_unixlike():
|
||||
system_users_granted = [s for s in v if s.protocol == 'ssh']
|
||||
system_users_granted = [s for s in v if s.protocol in ['ssh', 'telnet']]
|
||||
else:
|
||||
system_users_granted = [s for s in v if s.protocol == 'rdp']
|
||||
system_users_granted = [s for s in v if s.protocol in ['rdp', 'telnet']]
|
||||
k.system_users_granted = system_users_granted
|
||||
node.assets_granted = assets
|
||||
queryset.append(node)
|
||||
|
|
|
@ -6,13 +6,11 @@ from .. import views
|
|||
app_name = 'perms'
|
||||
|
||||
urlpatterns = [
|
||||
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/(?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})/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})/asset$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-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/(?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})/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})/asset/$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -173,14 +173,14 @@ function APIUpdateAttr(props) {
|
|||
}
|
||||
if (typeof props.success === 'function') {
|
||||
return props.success(data);
|
||||
}
|
||||
}
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
if (flash_message) {
|
||||
toastr.error(fail_message);
|
||||
}
|
||||
if (typeof props.error === 'function') {
|
||||
return props.error(jqXHR.responseText);
|
||||
}
|
||||
}
|
||||
});
|
||||
// return true;
|
||||
}
|
||||
|
@ -198,7 +198,8 @@ function objectDelete(obj, name, url, redirectTo) {
|
|||
}
|
||||
};
|
||||
var fail = function() {
|
||||
swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
||||
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
||||
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
|
@ -219,7 +220,7 @@ function objectDelete(obj, name, url, redirectTo) {
|
|||
confirmButtonText: '确认',
|
||||
closeOnConfirm: true,
|
||||
}, function () {
|
||||
doDelete()
|
||||
doDelete()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -272,7 +273,7 @@ jumpserver.initDataTable = function (options) {
|
|||
$(td).html('<input type="checkbox" class="text-center ipt_check" id=99991937>'.replace('99991937', cellData));
|
||||
}
|
||||
},
|
||||
{className: 'text-center', targets: '_all'}
|
||||
{className: 'text-center', render: $.fn.dataTable.render.text(), targets: '_all'}
|
||||
];
|
||||
columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs;
|
||||
var select = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="footer fixed">
|
||||
<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">-->
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -259,10 +259,35 @@ class SessionReplayViewSet(viewsets.ViewSet):
|
|||
serializer_class = ReplaySerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
session = None
|
||||
upload_to = 'replay' # 仅添加到本地存储中
|
||||
|
||||
def gen_session_path(self):
|
||||
def get_session_path(self, version=2):
|
||||
"""
|
||||
获取session日志的文件路径
|
||||
:param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz
|
||||
:return:
|
||||
"""
|
||||
suffix = '.replay.gz'
|
||||
if version == 1:
|
||||
suffix = '.gz'
|
||||
date = self.session.date_start.strftime('%Y-%m-%d')
|
||||
return os.path.join(date, str(self.session.id) + '.gz')
|
||||
return os.path.join(date, str(self.session.id) + suffix)
|
||||
|
||||
def get_local_path(self, version=2):
|
||||
session_path = self.get_session_path(version=version)
|
||||
if version == 2:
|
||||
local_path = os.path.join(self.upload_to, session_path)
|
||||
else:
|
||||
local_path = session_path
|
||||
return local_path
|
||||
|
||||
def save_to_storage(self, f):
|
||||
local_path = self.get_local_path()
|
||||
try:
|
||||
name = default_storage.save(local_path, f)
|
||||
return name, None
|
||||
except OSError as e:
|
||||
return None, e
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
session_id = kwargs.get('pk')
|
||||
|
@ -271,46 +296,49 @@ class SessionReplayViewSet(viewsets.ViewSet):
|
|||
|
||||
if serializer.is_valid():
|
||||
file = serializer.validated_data['file']
|
||||
file_path = self.gen_session_path()
|
||||
try:
|
||||
default_storage.save(file_path, file)
|
||||
return Response({'url': default_storage.url(file_path)},
|
||||
status=201)
|
||||
except IOError:
|
||||
return Response("Save error", status=500)
|
||||
name, err = self.save_to_storage(file)
|
||||
if not name:
|
||||
msg = "Failed save replay `{}`: {}".format(session_id, err)
|
||||
logger.error(msg)
|
||||
return Response({'msg': str(err)}, status=400)
|
||||
url = default_storage.url(name)
|
||||
return Response({'url': url}, status=201)
|
||||
else:
|
||||
logger.error(
|
||||
'Update load data invalid: {}'.format(serializer.errors))
|
||||
msg = 'Upload data invalid: {}'.format(serializer.errors)
|
||||
logger.error(msg)
|
||||
return Response({'msg': serializer.errors}, status=401)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
session_id = kwargs.get('pk')
|
||||
self.session = get_object_or_404(Session, id=session_id)
|
||||
path = self.gen_session_path()
|
||||
|
||||
if default_storage.exists(path):
|
||||
url = default_storage.url(path)
|
||||
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)
|
||||
# 新版本和老版本的文件后缀不同
|
||||
session_path = self.get_session_path() # 存在外部存储上的路径
|
||||
local_path = self.get_local_path()
|
||||
local_path_v1 = self.get_local_path(version=1)
|
||||
|
||||
if not configs:
|
||||
return HttpResponseNotFound()
|
||||
# 去default storage中查找
|
||||
for _local_path in (local_path, local_path_v1, session_path):
|
||||
if default_storage.exists(_local_path):
|
||||
url = default_storage.url(_local_path)
|
||||
return redirect(url)
|
||||
|
||||
date = self.session.date_start.strftime('%Y-%m-%d')
|
||||
file_path = os.path.join(date, str(self.session.id) + '.replay.gz')
|
||||
target_path = default_storage.base_location + '/' + path
|
||||
storage = jms_storage.get_multi_object_storage(configs)
|
||||
ok, err = storage.download(file_path, target_path)
|
||||
if ok:
|
||||
return redirect(default_storage.url(path))
|
||||
else:
|
||||
logger.error("Failed download replay file: {}".format(err))
|
||||
return HttpResponseNotFound()
|
||||
# 去定义的外部storage查找
|
||||
configs = settings.TERMINAL_REPLAY_STORAGE
|
||||
configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'}
|
||||
if not configs:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
target_path = os.path.join(default_storage.base_location, local_path) # 保存到storage的路径
|
||||
target_dir = os.path.dirname(target_path)
|
||||
if not os.path.isdir(target_dir):
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
storage = jms_storage.get_multi_object_storage(configs)
|
||||
ok, err = storage.download(session_path, target_path)
|
||||
if not ok:
|
||||
logger.error("Failed download replay file: {}".format(err))
|
||||
return HttpResponseNotFound()
|
||||
return redirect(default_storage.url(local_path))
|
||||
|
||||
|
||||
class SessionReplayV2ViewSet(SessionReplayViewSet):
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
<th class="text-center">{% trans 'System user' %}</th>
|
||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||
<th class="text-center">{% trans 'Login from' %}</th>
|
||||
<th class="text-center">{% trans 'Command' %}</th>
|
||||
<th class="text-center">{% trans 'Date start' %}</th>
|
||||
{# <th class="text-center">{% trans 'Date last active' %}</th>#}
|
||||
|
@ -92,6 +93,7 @@
|
|||
<td class="text-center">{{ session.system_user }}</td>
|
||||
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
|
||||
<td class="text-center">{{ session.protocol }}</td>
|
||||
<td class="text-center">{{ session.get_login_from_display }}</td>
|
||||
<td class="text-center">{{ session.id | get_session_command_amount }}</td>
|
||||
|
||||
<td class="text-center">{{ session.date_start }}</td>
|
||||
|
|
|
@ -3,6 +3,7 @@ import uuid
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
|
@ -14,10 +15,11 @@ from .serializers import UserSerializer, UserGroupSerializer, \
|
|||
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
|
||||
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
||||
from .tasks import write_login_log_async
|
||||
from .models import User, UserGroup
|
||||
from .models import User, UserGroup, LoginLog
|
||||
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
||||
IsSuperUserOrAppUser
|
||||
from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code
|
||||
from .utils import check_user_valid, generate_token, get_login_ip, \
|
||||
check_otp_code, set_user_login_failed_count_to_cache, is_block_login
|
||||
from common.mixins import IDInFilterMixin
|
||||
from common.utils import get_logger
|
||||
|
||||
|
@ -93,6 +95,22 @@ class UserUpdatePKApi(generics.UpdateAPIView):
|
|||
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):
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupSerializer
|
||||
|
@ -128,16 +146,12 @@ class UserToken(APIView):
|
|||
return Response({'error': msg}, status=406)
|
||||
|
||||
|
||||
class UserProfile(APIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
class UserProfile(generics.RetrieveAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get(self, request):
|
||||
# return Response(request.user.to_json())
|
||||
return Response(self.serializer_class(request.user).data)
|
||||
|
||||
def post(self, request):
|
||||
return Response(self.serializer_class(request.user).data)
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
class UserOtpAuthApi(APIView):
|
||||
|
@ -153,10 +167,23 @@ class UserOtpAuthApi(APIView):
|
|||
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
||||
|
||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_MFA,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
return Response({'msg': 'MFA认证失败'}, status=401)
|
||||
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
token = generate_token(request, user)
|
||||
self.write_login_log(request, user)
|
||||
return Response(
|
||||
{
|
||||
'token': token,
|
||||
|
@ -165,7 +192,7 @@ class UserOtpAuthApi(APIView):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, user):
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
@ -173,25 +200,54 @@ class UserOtpAuthApi(APIView):
|
|||
if not login_ip:
|
||||
login_ip = get_login_ip(request)
|
||||
|
||||
write_login_log_async.delay(
|
||||
user.username, ip=login_ip,
|
||||
type=login_type, user_agent=user_agent,
|
||||
)
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
class UserAuthApi(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserSerializer
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
if not user.otp_enabled:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
token = generate_token(request, user)
|
||||
self.write_login_log(request, user)
|
||||
return Response(
|
||||
{
|
||||
'token': token,
|
||||
|
@ -208,7 +264,8 @@ class UserAuthApi(APIView):
|
|||
'otp_url': reverse('api-users:user-otp-auth'),
|
||||
'seed': seed,
|
||||
'user': self.serializer_class(user).data
|
||||
}, status=300)
|
||||
}, status=300
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_user_valid(request):
|
||||
|
@ -222,7 +279,7 @@ class UserAuthApi(APIView):
|
|||
return user, msg
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, user):
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
@ -230,10 +287,14 @@ class UserAuthApi(APIView):
|
|||
if not login_ip:
|
||||
login_ip = get_login_ip(request)
|
||||
|
||||
write_login_log_async.delay(
|
||||
user.username, ip=login_ip,
|
||||
type=login_type, user_agent=user_agent,
|
||||
)
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent,
|
||||
}
|
||||
data.update(tmp_data)
|
||||
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
class UserConnectionTokenApi(APIView):
|
||||
|
|
|
@ -41,12 +41,40 @@ class LoginLog(models.Model):
|
|||
('W', 'Web'),
|
||||
('T', 'Terminal'),
|
||||
)
|
||||
|
||||
MFA_DISABLED = 0
|
||||
MFA_ENABLED = 1
|
||||
MFA_UNKNOWN = 2
|
||||
|
||||
MFA_CHOICE = (
|
||||
(MFA_DISABLED, _('Disabled')),
|
||||
(MFA_ENABLED, _('Enabled')),
|
||||
(MFA_UNKNOWN, _('-')),
|
||||
)
|
||||
|
||||
REASON_NOTHING = 0
|
||||
REASON_PASSWORD = 1
|
||||
REASON_MFA = 2
|
||||
|
||||
REASON_CHOICE = (
|
||||
(REASON_NOTHING, _('-')),
|
||||
(REASON_PASSWORD, _('Username/password check failed')),
|
||||
(REASON_MFA, _('MFA authentication failed')),
|
||||
)
|
||||
|
||||
STATUS_CHOICE = (
|
||||
(True, _('Success')),
|
||||
(False, _('Failed'))
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
username = models.CharField(max_length=20, verbose_name=_('Username'))
|
||||
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
|
||||
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
|
||||
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
|
||||
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
|
||||
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
|
||||
reason = models.SmallIntegerField(default=REASON_NOTHING, choices=REASON_CHOICE, verbose_name=_('Reason'))
|
||||
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
|
||||
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -45,13 +45,17 @@
|
|||
</div>
|
||||
<form class="m-t" role="form" method="post" action="">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
|
||||
{% if block_login %}
|
||||
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
||||
{% elif form.errors %}
|
||||
{% if 'captcha' in form.errors %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% else %}
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
|
||||
</div>
|
||||
|
|
|
@ -51,6 +51,9 @@
|
|||
<th class="text-center">{% trans 'UA' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'City' %}</th>
|
||||
<th class="text-center">{% trans 'MFA' %}</th>
|
||||
<th class="text-center">{% trans 'Reason' %}</th>
|
||||
<th class="text-center">{% trans 'Status' %}</th>
|
||||
<th class="text-center">{% trans 'Date' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -65,6 +68,9 @@
|
|||
</td>
|
||||
<td class="text-center">{{ login_log.ip }}</td>
|
||||
<td class="text-center">{{ login_log.city }}</td>
|
||||
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||
<td class="text-center">{{ login_log.datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -182,6 +182,14 @@
|
|||
</span>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -275,7 +283,7 @@ $(document).ready(function() {
|
|||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id];
|
||||
})
|
||||
});
|
||||
})
|
||||
.on('click', '#is_active', function() {
|
||||
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() {
|
||||
{% if request.user == user_object %}
|
||||
toastr.error("{% trans 'Goto profile page enable MFA' %}");
|
||||
return
|
||||
return;
|
||||
{% endif %}
|
||||
|
||||
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});
|
||||
}).on('click', '.btn-delete-user', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ user.name }}";
|
||||
var uid = "{{ user.id }}";
|
||||
var name = "{{ user_object.name }}";
|
||||
var uid = "{{ user_object.id }}";
|
||||
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
var redirect_url = "{% url 'users:user-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
}).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>
|
||||
{% endblock %}
|
||||
|
|
|
@ -59,7 +59,7 @@ function initTable() {
|
|||
ele: $('#user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + escape(cellData) + '</a>';
|
||||
$(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
|
|
|
@ -29,6 +29,8 @@ urlpatterns = [
|
|||
api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
||||
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/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/$',
|
||||
api.UserUpdateGroupApi.as_view(), name='user-update-group'),
|
||||
url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$',
|
||||
|
|
|
@ -8,13 +8,13 @@ app_name = 'users'
|
|||
|
||||
urlpatterns = [
|
||||
# Login view
|
||||
url(r'^login$', views.UserLoginView.as_view(), name='login'),
|
||||
url(r'^logout$', views.UserLogoutView.as_view(), name='logout'),
|
||||
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/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/success$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
url(r'^login/$', views.UserLoginView.as_view(), name='login'),
|
||||
url(r'^logout/$', views.UserLogoutView.as_view(), name='logout'),
|
||||
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/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/success/$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
|
||||
# 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'),
|
||||
|
||||
# User view
|
||||
url(r'^user$', views.UserListView.as_view(), name='user-list'),
|
||||
url(r'^user/export/', views.UserExportView.as_view(), name='user-export'),
|
||||
url(r'^user/$', views.UserListView.as_view(), name='user-list'),
|
||||
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'^user/import/$', views.UserBulkImportView.as_view(), name='user-import'),
|
||||
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/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})/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/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/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})/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'),
|
||||
|
||||
# User group view
|
||||
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/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})/assets', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
||||
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/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})/assets/$', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
||||
|
||||
# Login log
|
||||
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.conf import settings
|
||||
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.core.cache import cache
|
||||
|
||||
|
@ -200,16 +200,15 @@ def get_login_ip(request):
|
|||
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)):
|
||||
ip = ip[:15]
|
||||
city = "Unknown"
|
||||
else:
|
||||
city = get_ip_city(ip)
|
||||
LoginLog.objects.create(
|
||||
username=username, type=type,
|
||||
ip=ip, city=city, user_agent=user_agent
|
||||
)
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
LoginLog.objects.create(**kwargs)
|
||||
|
||||
|
||||
def get_ip_city(ip, timeout=10):
|
||||
|
@ -332,3 +331,44 @@ def check_password_rules(password):
|
|||
|
||||
match_obj = re.match(pattern, password)
|
||||
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.models import Setting
|
||||
from ..models import User, LoginLog
|
||||
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \
|
||||
get_user_or_tmp_user, set_tmp_user_to_cache, get_password_check_rules, check_password_rules
|
||||
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
|
||||
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 .. import forms
|
||||
|
||||
|
@ -47,7 +49,9 @@ class UserLoginView(FormView):
|
|||
form_class = forms.UserLoginForm
|
||||
form_class_captcha = forms.UserLoginCaptchaForm
|
||||
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):
|
||||
if request.user.is_staff:
|
||||
|
@ -57,6 +61,16 @@ class UserLoginView(FormView):
|
|||
request.session.set_test_cookie()
|
||||
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):
|
||||
if not self.request.session.test_cookie_worked():
|
||||
return HttpResponse(_("Please enable cookies and try again."))
|
||||
|
@ -65,8 +79,24 @@ class UserLoginView(FormView):
|
|||
return redirect(self.get_success_url())
|
||||
|
||||
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)
|
||||
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
|
||||
form = self.form_class_captcha(data=form.data)
|
||||
form._errors = old_form.errors
|
||||
|
@ -74,7 +104,7 @@ class UserLoginView(FormView):
|
|||
|
||||
def get_form_class(self):
|
||||
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
|
||||
else:
|
||||
return self.form_class
|
||||
|
@ -91,7 +121,13 @@ class UserLoginView(FormView):
|
|||
elif not user.otp_enabled:
|
||||
# 0 & T,F
|
||||
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)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -101,13 +137,16 @@ class UserLoginView(FormView):
|
|||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def write_login_log(self):
|
||||
def write_login_log(self, data):
|
||||
login_ip = get_login_ip(self.request)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
write_login_log_async.delay(
|
||||
self.request.user.username, type='W',
|
||||
ip=login_ip, user_agent=user_agent
|
||||
)
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': 'W',
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
class UserLoginOtpView(FormView):
|
||||
|
@ -122,22 +161,38 @@ class UserLoginOtpView(FormView):
|
|||
|
||||
if check_otp_code(otp_secret_key, otp_code):
|
||||
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())
|
||||
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'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
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)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
write_login_log_async.delay(
|
||||
self.request.user.username, type='W',
|
||||
ip=login_ip, user_agent=user_agent
|
||||
)
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': 'W',
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
@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 .. import forms
|
||||
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 ..tasks import write_login_log_async
|
||||
|
||||
|
@ -168,13 +170,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
|
|||
model = User
|
||||
template_name = 'users/user_detail.html'
|
||||
context_object_name = "user_object"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
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())
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('User detail'),
|
||||
'groups': groups
|
||||
'groups': groups,
|
||||
'unblock': is_need_unblock(key_block),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
|
|
@ -21,10 +21,10 @@ class Config:
|
|||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# 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/
|
||||
LOG_LEVEL = 'DEBUG'
|
||||
LOG_LEVEL = os.environ.get("LOG_LEVEL") or 'DEBUG'
|
||||
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
||||
|
||||
# Database setting, Support sqlite3, mysql, postgres ....
|
||||
|
@ -35,12 +35,12 @@ class Config:
|
|||
DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3')
|
||||
|
||||
# MySQL or postgres setting like:
|
||||
# DB_ENGINE = 'mysql'
|
||||
# DB_HOST = '127.0.0.1'
|
||||
# DB_PORT = 3306
|
||||
# DB_USER = 'root'
|
||||
# DB_PASSWORD = ''
|
||||
# DB_NAME = 'jumpserver'
|
||||
# DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql'
|
||||
# DB_HOST = os.environ.get("DB_HOST") or '127.0.0.1'
|
||||
# DB_PORT = os.environ.get("DB_PORT") or 3306
|
||||
# DB_USER = os.environ.get("DB_USER") or 'jumpserver'
|
||||
# DB_PASSWORD = os.environ.get("DB_PASSWORD") or 'weakPassword'
|
||||
# DB_NAME = os.environ.get("DB_NAME") or 'jumpserver'
|
||||
|
||||
# When Django start it will bind this host and port
|
||||
# ./manage.py runserver 127.0.0.1:8080
|
||||
|
@ -48,9 +48,11 @@ class Config:
|
|||
HTTP_LISTEN_PORT = 8080
|
||||
|
||||
# Use Redis as broker for celery and web socket
|
||||
REDIS_HOST = '127.0.0.1'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_PASSWORD = ''
|
||||
REDIS_HOST = os.environ.get("REDIS_HOST") or '127.0.0.1'
|
||||
REDIS_PORT = os.environ.get("REDIS_PORT") or 6379
|
||||
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):
|
||||
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
|
||||
redis==2.10.6
|
||||
requests==2.18.4
|
||||
jms-storage==0.0.17
|
||||
jms-storage==0.0.18
|
||||
s3transfer==0.1.13
|
||||
simplejson==3.13.2
|
||||
six==1.11.0
|
||||
|
|
|
@ -4,3 +4,5 @@
|
|||
python3 ../apps/manage.py makemigrations
|
||||
|
||||
python3 ../apps/manage.py migrate
|
||||
|
||||
python3 ../apps/manage.py makemigrations --merge
|
||||
|
|
Loading…
Reference in New Issue