diff --git a/README.md b/README.md
index df8324761..f0bceec9b 100644
--- a/README.md
+++ b/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交互,
diff --git a/apps/__init__.py b/apps/__init__.py
index 7e96164aa..be40e1dd2 100644
--- a/apps/__init__.py
+++ b/apps/__init__.py
@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
#
-__version__ = "1.3.2"
+__version__ = "1.3.3"
diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py
index ff5047ba3..8c1f3d726 100644
--- a/apps/assets/api/asset.py
+++ b/apps/assets/api/asset.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
#
+import random
+
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
@@ -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)
\ No newline at end of file
diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py
index f8f187b4d..5000c087d 100644
--- a/apps/assets/forms/asset.py
+++ b/apps/assets/forms/asset.py
@@ -16,7 +16,7 @@ class AssetCreateForm(forms.ModelForm):
fields = [
'hostname', 'ip', 'public_ip', 'port', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
- 'domain',
+ 'domain', 'protocol',
]
widgets = {
@@ -56,7 +56,7 @@ class AssetUpdateForm(forms.ModelForm):
fields = [
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
'public_ip', 'number', 'comment', 'admin_user', 'labels',
- 'domain',
+ 'domain', 'protocol',
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py
index 2295dc005..b25e19d87 100644
--- a/apps/assets/forms/user.py
+++ b/apps/assets/forms/user.py
@@ -93,14 +93,21 @@ class SystemUserForm(PasswordAndKeyAuthForm):
# Because we define custom field, so we need rewrite :method: `save`
system_user = super().save()
password = self.cleaned_data.get('password', '') or None
+ login_mode = self.cleaned_data.get('login_mode', '') or None
+ protocol = self.cleaned_data.get('protocol') or None
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys()
+ if login_mode == SystemUser.MANUAL_LOGIN or protocol == SystemUser.TELNET_PROTOCOL:
+ system_user.auto_push = 0
+ system_user.save()
+
if auto_generate_key:
logger.info('Auto generate key and set system user auth')
system_user.auto_gen_auth()
else:
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
+
return system_user
def clean(self):
@@ -109,12 +116,24 @@ class SystemUserForm(PasswordAndKeyAuthForm):
if not self.instance and not auto_generate:
super().validate_password_key()
+ def is_valid(self):
+ validated = super().is_valid()
+ username = self.cleaned_data.get('username')
+ login_mode = self.cleaned_data.get('login_mode')
+ if login_mode == SystemUser.AUTO_LOGIN and not username:
+ self.add_error(
+ "username", _('* Automatic login mode,'
+ ' must fill in the username.')
+ )
+ return False
+ return validated
+
class Meta:
model = SystemUser
fields = [
'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key_file', 'auto_push', 'sudo',
- 'comment', 'shell', 'priority',
+ 'comment', 'shell', 'priority', 'login_mode',
]
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
@@ -124,5 +143,8 @@ class SystemUserForm(PasswordAndKeyAuthForm):
'name': '* required',
'username': '* required',
'auto_push': _('Auto push system user to asset'),
- 'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'),
- }
\ No newline at end of file
+ 'priority': _('High level will be using login asset as default, '
+ 'if user was granted more than 2 system user'),
+ 'login_mode': _('If you choose manual login mode, you do not '
+ 'need to fill in the username and password.')
+ }
diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py
index a974d3385..7a2b3fe57 100644
--- a/apps/assets/models/asset.py
+++ b/apps/assets/models/asset.py
@@ -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'))
diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py
index cb9bb96ae..908e6b647 100644
--- a/apps/assets/models/base.py
+++ b/apps/assets/models/base.py
@@ -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'))
diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py
index bf31b8491..5faca5da8 100644
--- a/apps/assets/models/user.py
+++ b/apps/assets/models/user.py
@@ -95,9 +95,18 @@ class AdminUser(AssetUser):
class SystemUser(AssetUser):
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
+ TELNET_PROTOCOL = 'telnet'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
+ (TELNET_PROTOCOL, 'telnet (beta)'),
+ )
+
+ AUTO_LOGIN = 'auto'
+ MANUAL_LOGIN = 'manual'
+ LOGIN_MODE_CHOICES = (
+ (AUTO_LOGIN, _('Automatic login')),
+ (MANUAL_LOGIN, _('Manually login'))
)
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
@@ -107,6 +116,7 @@ class SystemUser(AssetUser):
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
+ login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode'))
def __str__(self):
return '{0.name}({0.username})'.format(self)
diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
index a0fdfab73..e63735794 100644
--- a/apps/assets/serializers/asset.py
+++ b/apps/assets/serializers/asset.py
@@ -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
diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py
index 7abd09d29..a295f245c 100644
--- a/apps/assets/serializers/system_user.py
+++ b/apps/assets/serializers/system_user.py
@@ -18,6 +18,13 @@ class SystemUserSerializer(serializers.ModelSerializer):
model = SystemUser
exclude = ('_password', '_private_key', '_public_key')
+ def get_field_names(self, declared_fields, info):
+ fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
+ fields.extend([
+ 'get_login_mode_display',
+ ])
+ return fields
+
@staticmethod
def get_unreachable_assets(obj):
return obj.unreachable_assets
@@ -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):
diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html
index 314967d22..4e1bc51a8 100644
--- a/apps/assets/templates/assets/_system_user.html
+++ b/apps/assets/templates/assets/_system_user.html
@@ -36,12 +36,13 @@
{% endif %}
{% trans 'Basic' %}
{% bootstrap_field form.name layout="horizontal" %}
+ {% bootstrap_field form.login_mode layout="horizontal" %}
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.priority layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
+ {% trans 'Auth' %}
{% block auth %}
- {% trans 'Auth' %}
@@ -80,15 +81,22 @@
{% endblock %}
{% block custom_foot_js %}
{% endblock %}
\ No newline at end of file
diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html
index bcb40fb29..15a870800 100644
--- a/apps/assets/templates/assets/admin_user_list.html
+++ b/apps/assets/templates/assets/admin_user_list.html
@@ -5,7 +5,7 @@
{% block help_message %}
- 管理用户是服务器的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
+ 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
Windows或其它硬件可以随意设置一个
{% endblock %}
@@ -107,6 +107,3 @@ $(document).ready(function(){
});
{% endblock %}
-
-
-
diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html
index 99703d2e3..55e233d0d 100644
--- a/apps/assets/templates/assets/asset_create.html
+++ b/apps/assets/templates/assets/asset_create.html
@@ -17,6 +17,7 @@
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
+ {% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %}
@@ -85,14 +86,14 @@ $(document).ready(function () {
allowClear: true,
templateSelection: format
});
- $("#id_platform").change(function (){
- var platform = $("#id_platform option:selected").text();
+ $("#id_protocol").change(function (){
+ var protocol = $("#id_protocol option:selected").text();
var port = 22;
- if(platform === 'Windows'){
+ if(protocol === 'rdp'){
port = 3389;
}
- if(platform === 'Other'){
- port = null;
+ if(protocol === 'telnet (beta)'){
+ port = 23;
}
$("#id_port").val(port);
});
diff --git a/apps/assets/templates/assets/asset_update.html b/apps/assets/templates/assets/asset_update.html
index 3d42ca2b5..7ed1da05a 100644
--- a/apps/assets/templates/assets/asset_update.html
+++ b/apps/assets/templates/assets/asset_update.html
@@ -21,6 +21,7 @@
{% trans 'Basic' %}
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
+ {% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
diff --git a/apps/assets/templates/assets/domain_list.html b/apps/assets/templates/assets/domain_list.html
index 926c4bbc3..a4042d57e 100644
--- a/apps/assets/templates/assets/domain_list.html
+++ b/apps/assets/templates/assets/domain_list.html
@@ -1,6 +1,14 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
+
+{% block help_message %}
+
+ 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登
+录。
+
+{% endblock %}
+
{% block table_container %}
{% trans "Create domain" %}
@@ -69,6 +77,3 @@ $(document).ready(function(){
});
{% endblock %}
-
-
-
diff --git a/apps/assets/templates/assets/gateway_create_update.html b/apps/assets/templates/assets/gateway_create_update.html
index e0b10ba73..fea540385 100644
--- a/apps/assets/templates/assets/gateway_create_update.html
+++ b/apps/assets/templates/assets/gateway_create_update.html
@@ -42,7 +42,7 @@
{% bootstrap_field form.domain layout="horizontal" %}
{% block auth %}
-
{% trans 'Auth' %}
+
{% trans 'Auth' %}
{% 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');
}
}
diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html
index 9a7bc255a..dd188bdab 100644
--- a/apps/assets/templates/assets/system_user_detail.html
+++ b/apps/assets/templates/assets/system_user_detail.html
@@ -62,6 +62,10 @@
{% trans 'Username' %}: |
{{ system_user.username }} |
+
+ {% trans 'Login mode' %}: |
+ {{ system_user.get_login_mode_display }} |
+
{% trans 'Protocol' %}: |
{{ system_user.protocol }} |
@@ -148,15 +152,14 @@
-
-
- {% trans 'Clear auth' %}: |
-
-
-
-
- |
-
+{#
#}
+{# {% trans 'Clear auth' %}: | #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# | #}
+{#
#}
{#
#}
{# {% trans 'Change auth period' %}: | #}
@@ -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' %}"
+ });
});
})
diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html
index 3daa47a81..3fa887df6 100644
--- a/apps/assets/templates/assets/system_user_list.html
+++ b/apps/assets/templates/assets/system_user_list.html
@@ -26,6 +26,7 @@
{% trans 'Name' %} |
{% trans 'Username' %} |
{% trans 'Protocol' %} |
+ {% trans 'Login mode' %} |
{% trans 'Asset' %} |
{% trans 'Reachable' %} |
{% trans 'Unreachable' %} |
@@ -48,7 +49,7 @@ function initTable() {
var detail_btn = '' + cellData + '';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
- {targets: 5, createdCell: function (td, cellData) {
+ {targets: 6, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "" + cellData + "";
@@ -57,7 +58,7 @@ function initTable() {
}
$(td).html('' + innerHtml + '');
}},
- {targets: 6, createdCell: function (td, cellData) {
+ {targets: 7, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "" + cellData + "";
@@ -66,7 +67,7 @@ function initTable() {
}
$(td).html('' + innerHtml + '');
}},
- {targets: 7, createdCell: function (td, cellData, rowData) {
+ {targets: 8, createdCell: function (td, cellData, rowData) {
var val = 0;
var innerHtml = "";
var total = rowData.assets_amount;
@@ -84,14 +85,14 @@ function initTable() {
$(td).html('' + innerHtml + '');
}},
- {targets: 9, createdCell: function (td, cellData, rowData) {
+ {targets: 10, createdCell: function (td, cellData, rowData) {
var update_btn = '{% trans "Update" %}'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}],
ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [
- {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "assets_amount" },
+ {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
],
op_html: $('#actions').html()
diff --git a/apps/assets/templates/assets/system_user_update.html b/apps/assets/templates/assets/system_user_update.html
index 7e1590db5..977c3ac31 100644
--- a/apps/assets/templates/assets/system_user_update.html
+++ b/apps/assets/templates/assets/system_user_update.html
@@ -4,7 +4,6 @@
{% load bootstrap3 %}
{% block auth %}
- {% trans 'Auth' %}
{% bootstrap_field form.password layout="horizontal" %}
{% bootstrap_field form.private_key_file layout="horizontal" %}
{% endfor %}
diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html
index 22ab4b5d4..b664d917f 100644
--- a/apps/users/templates/users/user_detail.html
+++ b/apps/users/templates/users/user_detail.html
@@ -182,6 +182,14 @@
+
+ {% trans 'Unblock user' %} |
+
+
+
+
+ |
+
@@ -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();
+ });
})
{% endblock %}
diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html
index 052c5b7c0..27c07c538 100644
--- a/apps/users/templates/users/user_list.html
+++ b/apps/users/templates/users/user_list.html
@@ -59,7 +59,7 @@ function initTable() {
ele: $('#user_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
- var detail_btn = '
' + cellData + '';
+ var detail_btn = '
' + escape(cellData) + '';
$(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id));
}},
{targets: 4, createdCell: function (td, cellData) {
diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py
index 683638a4e..017224421 100644
--- a/apps/users/urls/api_urls.py
+++ b/apps/users/urls/api_urls.py
@@ -29,6 +29,8 @@ urlpatterns = [
api.UserResetPKApi.as_view(), name='user-public-key-reset'),
url(r'^v1/users/(?P
[0-9a-zA-Z\-]{36})/pubkey/update/$',
api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
+ url(r'^v1/users/(?P[0-9a-zA-Z\-]{36})/unblock/$',
+ api.UserUnblockPKApi.as_view(), name='user-unblock'),
url(r'^v1/users/(?P[0-9a-zA-Z\-]{36})/groups/$',
api.UserUpdateGroupApi.as_view(), name='user-update-group'),
url(r'^v1/groups/(?P[0-9a-zA-Z\-]{36})/users/$',
diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py
index 2aaf4a9ae..8052f1384 100644
--- a/apps/users/urls/views_urls.py
+++ b/apps/users/urls/views_urls.py
@@ -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[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[0-9a-zA-Z\-]{36})$', views.UserDetailView.as_view(), name='user-detail'),
- url(r'^user/(?P[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
- url(r'^user/(?P[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[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[0-9a-zA-Z\-]{36})/$', views.UserDetailView.as_view(), name='user-detail'),
+ url(r'^user/(?P[0-9a-zA-Z\-]{36})/assets/$', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
+ url(r'^user/(?P[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[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[0-9a-zA-Z\-]{36})/update$', views.UserGroupUpdateView.as_view(), name='user-group-update'),
- url(r'^user-group/(?P[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[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[0-9a-zA-Z\-]{36})/update/$', views.UserGroupUpdateView.as_view(), name='user-group-update'),
+ url(r'^user-group/(?P[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'),
diff --git a/apps/users/utils.py b/apps/users/utils.py
index 989632e2c..7cbaa75f0 100644
--- a/apps/users/utils.py
+++ b/apps/users/utils.py
@@ -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
diff --git a/apps/users/views/login.py b/apps/users/views/login.py
index feaf47e89..fc7caf305 100644
--- a/apps/users/views/login.py
+++ b/apps/users/views/login.py
@@ -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')
diff --git a/apps/users/views/user.py b/apps/users/views/user.py
index 094d2ced2..56d551efa 100644
--- a/apps/users/views/user.py
+++ b/apps/users/views/user.py
@@ -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)
diff --git a/config_example.py b/config_example.py
index 0c8d87094..a96f0d7c9 100644
--- a/config_example.py
+++ b/config_example.py
@@ -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
diff --git a/requirements/deb_requirements.txt b/requirements/deb_requirements.txt
index a0ddb7642..f4131a3ea 100644
--- a/requirements/deb_requirements.txt
+++ b/requirements/deb_requirements.txt
@@ -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
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index bcba2627c..25d28f259 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -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
diff --git a/utils/make_migrations.sh b/utils/make_migrations.sh
index fdf1f6efb..4fb8fdacf 100755
--- a/utils/make_migrations.sh
+++ b/utils/make_migrations.sh
@@ -4,3 +4,5 @@
python3 ../apps/manage.py makemigrations
python3 ../apps/manage.py migrate
+
+python3 ../apps/manage.py makemigrations --merge