Commit ee35ca36 authored by BaiJiangJie's avatar BaiJiangJie

[Feature] 增加功能,安全设置(全局MFA设置,密码强度校验)

parent 526943a0
...@@ -168,3 +168,48 @@ class TerminalSettingForm(BaseForm): ...@@ -168,3 +168,48 @@ class TerminalSettingForm(BaseForm):
) )
) )
class SecuritySettingForm(BaseForm):
# MFA全局设置
SECURITY_MFA_AUTH = forms.BooleanField(
initial=False, required=False,
label=_("MFA Secondary certification"),
help_text=_(
'After opening, the user login must use MFA secondary '
'authentication (valid for all users, including administrators)'
)
)
# 最小长度
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"),
)
# 大写字母
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
initial=False, required=False,
label=_("Must contain capital letters"),
help_text=_(
'After opening, the user password changes '
'and resets must contain uppercase letters')
)
# 小写字母
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')
)
# 数字
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')
)
# 特殊字符
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
initial=False, required=False,
label=_("Must contain special characters"),
help_text=_('After opening, the user password changes '
'and resets must contain special characters')
)
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
<li> <li>
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a> <a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
<li> <li>
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a> <a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
<li> <li>
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a> <a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
......
{% extends 'base.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% load common_tags %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Basic setting' %}</a>
</li>
<li>
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li>
<li>
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
</li>
<li>
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
</li>
<li class="active">
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-12" style="padding-left:0">
<div class="ibox-content" style="border-width: 0;padding-top: 40px;">
<form action="" method="post" class="form-horizontal">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% csrf_token %}
<h3>{% trans "MFA setting" %}</h3>
{% for field in form %}
{% if forloop.counter == 2 %}
<div class="hr-line-dashed"></div>
<h3>{% trans "Password check rule" %}</h3>
{% endif %}
{% if not field.field|is_bool_field %}
{% bootstrap_field field layout="horizontal" %}
{% else %}
<div class="form-group">
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-8">
<div class="col-sm-1">
{{ field }}
</div>
<div class="col-sm-9">
<span class="help-block" >{{ field.help_text }}</span>
</div>
</div>
</div>
{% endif %}
{% endfor %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
})
.on("click", ".btn-test", function () {
var data = {};
var form = $("form").serializeArray();
$.each(form, function (i, field) {
data[field.name] = field.value;
});
var the_url = "{% url 'api-common:mail-testing' %}";
function error(message) {
toastr.error(message)
}
function success(message) {
toastr.success(message.msg)
}
APIUpdateAttr({
url: the_url,
body: JSON.stringify(data),
method: "POST",
flash_message: false,
success: success,
error: error
});
})
</script>
{% endblock %}
...@@ -27,6 +27,9 @@ ...@@ -27,6 +27,9 @@
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i <a href="{% url 'settings:terminal-setting' %}" class="text-center"><i
class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a> class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
...@@ -39,6 +42,7 @@ ...@@ -39,6 +42,7 @@
</div> </div>
{% endif %} {% endif %}
{% csrf_token %} {% csrf_token %}
<h3>{% trans "Basic setting" %}</h3> <h3>{% trans "Basic setting" %}</h3>
{% for field in form %} {% for field in form %}
{% if not field.field|is_bool_field %} {% if not field.field|is_bool_field %}
...@@ -60,6 +64,7 @@ ...@@ -60,6 +64,7 @@
{% endfor %} {% endfor %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Command storage" %}</h3> <h3>{% trans "Command storage" %}</h3>
<table class="table table-hover " id="task-history-list-table"> <table class="table table-hover " id="task-history-list-table">
<thead> <thead>
......
...@@ -11,4 +11,5 @@ urlpatterns = [ ...@@ -11,4 +11,5 @@ urlpatterns = [
url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'),
url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'),
url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'), url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'),
url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'),
] ]
...@@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _ ...@@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm TerminalSettingForm, SecuritySettingForm
from .mixins import AdminUserRequiredMixin from .mixins import AdminUserRequiredMixin
from .signals import ldap_auth_enable from .signals import ldap_auth_enable
...@@ -122,3 +122,27 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView): ...@@ -122,3 +122,27 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
class SecuritySettingView(AdminUserRequiredMixin, TemplateView):
form_class = SecuritySettingForm
template_name = "common/security_setting.html"
def get_context_data(self, **kwargs):
context = {
'app': _('Settings'),
'action': _('Security setting'),
'form': self.form_class(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def post(self, request):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
msg = _("Update setting successfully, please restart program")
messages.success(request, msg)
return redirect('settings:security-setting')
else:
context = self.get_context_data()
context.update({"form": form})
return render(request, self.template_name, context)
This diff is collapsed.
...@@ -401,6 +401,9 @@ TERMINAL_REPLAY_STORAGE = { ...@@ -401,6 +401,9 @@ TERMINAL_REPLAY_STORAGE = {
}, },
} }
DEFAULT_PASSWORD_MIN_LENGTH = 6
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
'horizontal_label_class': 'col-md-2', 'horizontal_label_class': 'col-md-2',
......
...@@ -609,3 +609,91 @@ function setUrlParam(url, name, value) { ...@@ -609,3 +609,91 @@ function setUrlParam(url, name, value) {
} }
return url return url
} }
// 校验密码-改变规则颜色
function checkPasswordRules(password, minLength) {
if (wordMinLength(password, minLength)) {
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', '#908a8a')
}
if (wordUpperCase(password)) {
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', 'green');
}
else {
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', '#908a8a')
}
if (wordLowerCase(password)) {
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', '#908a8a')
}
if (wordNumber(password)) {
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', '#908a8a')
}
if (wordSpecialChar(password)) {
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', '#908a8a')
}
}
// 最小长度
function wordMinLength(word, minLength) {
//var minLength = {{ min_length }};
var re = new RegExp("^(.{" + minLength + ",})$");
return word.match(re)
}
// 大写字母
function wordUpperCase(word) {
return word.match(/([A-Z]+)/)
}
// 小写字母
function wordLowerCase(word) {
return word.match(/([a-z]+)/)
}
// 数字字符
function wordNumber(word) {
return word.match(/([\d]+)/)
}
// 特殊字符
function wordSpecialChar(word) {
return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/)
}
// 显示弹窗密码规则
function popoverPasswordRules(password_check_rules, $el) {
var message = "";
jQuery.each(password_check_rules, function (idx, rules) {
message += "<li id=" + rules.id + " style='list-style-type:none;'> <i class='fa fa-check-circle-o' style='margin-right:10px;' ></i>" + rules.label + "</li>";
});
//$('#id_password_rules').html(message);
$el.html(message)
}
// 初始化弹窗popover
function initPopover($container, $progress, $idPassword, $el, password_check_rules){
options = {};
// User Interface
options.ui = {
container: $container,
viewports: {
progress: $progress
//errors: $('.popover-content')
},
showProgressbar: true,
showVerdictsInsideProgressBar: true
};
$idPassword.pwstrength(options);
popoverPasswordRules(password_check_rules, $el);
}
This diff is collapsed.
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script> <script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
{% block custom_head_css_js_create %} {% endblock %} {% block custom_head_css_js_create %} {% endblock %}
{% endblock %} {% endblock %}
......
...@@ -72,7 +72,7 @@ class UserCreateUpdateForm(forms.ModelForm): ...@@ -72,7 +72,7 @@ class UserCreateUpdateForm(forms.ModelForm):
'data-placeholder': _('Join user groups') 'data-placeholder': _('Join user groups')
} }
), ),
'otp_level': forms.RadioSelect() 'otp_level': forms.RadioSelect(),
} }
def clean_public_key(self): def clean_public_key(self):
......
...@@ -48,6 +48,7 @@ ...@@ -48,6 +48,7 @@
</div> </div>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
{% include '_head_css_js.html' %} {% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet"> <link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script src="{% static "js/jumpserver.js" %}"></script> <script src="{% static "js/jumpserver.js" %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
</head> </head>
...@@ -49,10 +50,20 @@ ...@@ -49,10 +50,20 @@
<p class="red-fonts">{{ errors }}</p> <p class="red-fonts">{{ errors }}</p>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required=""> <input type="password" id="id_new_password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required="">
{# 密码popover #}
<div id="container">
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
<div class="arrow" style="left: 50%;"></div>
<h3 class="popover-title" style="display: none;"></h3>
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
<div class="popover-content"></div>
</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="password-confirm" placeholder="{% trans 'Password again' %}" required=""> <input type="password" id="id_confirm_password" class="form-control" name="password-confirm" placeholder="{% trans 'Password again' %}" required="">
</div> </div>
<button type="submit" class="btn btn-primary block full-width m-b">{% trans "Setting" %}</button> <button type="submit" class="btn btn-primary block full-width m-b">{% trans "Setting" %}</button>
...@@ -79,4 +90,33 @@ ...@@ -79,4 +90,33 @@
</body> </body>
</html> </html>
<script>
$(document).ready(function () {
// 密码强度校验
var el = $('#id_password_rules'),
idPassword = $('#id_new_password'),
idPopover = $('#popover777'),
container = $('#container'),
progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }},
top = 146, left = 170;
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules);
// 监听事件
idPassword.on('focus', function () {
idPopover.css('top', top);
idPopover.css('left', left);
idPopover.css('display', 'block');
});
idPassword.on('blur', function () {
idPopover.css('display', 'none');
});
idPassword.on('keyup', function(){
var password = idPassword.val();
checkPasswordRules(password, minLength);
})
})
</script>
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
<link href="{% static "css/plugins/cropper/cropper.min.css" %}" rel="stylesheet"> <link href="{% static "css/plugins/cropper/cropper.min.css" %}" rel="stylesheet">
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet"> <link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script> <script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style> <style>
.crop { .crop {
...@@ -50,6 +52,16 @@ ...@@ -50,6 +52,16 @@
{% csrf_token %} {% csrf_token %}
{% bootstrap_field form.old_password layout="horizontal" %} {% bootstrap_field form.old_password layout="horizontal" %}
{% bootstrap_field form.new_password layout="horizontal" %} {% bootstrap_field form.new_password layout="horizontal" %}
{# 密码popover #}
<div id="container">
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
<div class="arrow" style="left: 50%;"></div>
<h3 class="popover-title" style="display: none;"></h3>
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
<div class="popover-content"></div>
</div>
</div>
{% bootstrap_field form.confirm_password layout="horizontal" %} {% bootstrap_field form.confirm_password layout="horizontal" %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
...@@ -71,5 +83,34 @@ ...@@ -71,5 +83,34 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/cropper/cropper.min.js' %}"></script> <script src="{% static 'js/plugins/cropper/cropper.min.js' %}"></script>
<script> <script>
$(document).ready(function () {
var el = $('#id_password_rules'),
idPassword = $('#id_new_password'),
idPopover = $('#popover777'),
container = $('#container'),
progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }},
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
left = 377;
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules);
// 监听事件
idPassword.on('focus', function () {
idPopover.css('top', top);
idPopover.css('left', left);
idPopover.css('display', 'block');
});
idPassword.on('blur', function () {
idPopover.css('display', 'none');
});
idPassword.on('keyup', function(){
var password = idPassword.val();
checkPasswordRules(password, minLength);
});
})
</script> </script>
{% endblock %} {% endblock %}
...@@ -91,6 +91,9 @@ ...@@ -91,6 +91,9 @@
{% else %} {% else %}
{% trans 'Disable' %} {% trans 'Disable' %}
{% endif %} {% endif %}
{% if mfa_setting %}
( {% trans 'Administrator Settings force MFA login' %} )
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
...@@ -152,7 +155,7 @@ ...@@ -152,7 +155,7 @@
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id="" <a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
href=" href="
{% if request.user.otp_enabled and request.user.otp_secret_key %} {% if request.user.otp_enabled and request.user.otp_secret_key %}
{% if request.user.otp_force_enabled %} {% if request.user.otp_force_enabled or mfa_setting %}
" disabled >{% trans 'Disable' %} " disabled >{% trans 'Disable' %}
{% else %} {% else %}
{% url 'users:user-otp-disable-authentication' %} {% url 'users:user-otp-disable-authentication' %}
......
...@@ -4,5 +4,50 @@ ...@@ -4,5 +4,50 @@
{% block user_template_title %}{% trans "Update user" %}{% endblock %} {% block user_template_title %}{% trans "Update user" %}{% endblock %}
{% block password %} {% block password %}
{% bootstrap_field form.password layout="horizontal" %} {% bootstrap_field form.password layout="horizontal" %}
{# 密码popover #}
<div id="container">
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
<div class="arrow" style="left: 50%;"></div>
<h3 class="popover-title" style="display: none;"></h3>
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
<div class="popover-content"></div>
</div>
</div>
{% bootstrap_field form.public_key layout="horizontal" %} {% bootstrap_field form.public_key layout="horizontal" %}
{% endblock %} {% endblock %}
{% block custom_foot_js %}
{{ block.super }}
<script>
$(document).ready(function(){
var el = $('#id_password_rules'),
idPassword = $('#id_password'),
idPopover = $('#popover777'),
container = $('#container'),
progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }},
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
left = 377;
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules);
// 监听事件
idPassword.on('focus', function () {
idPopover.css('top', top);
idPopover.css('left', left);
idPopover.css('display', 'block');
});
idPassword.on('blur', function () {
idPopover.css('display', 'none');
});
idPassword.on('keyup', function(){
var password = idPassword.val();
checkPasswordRules(password, minLength);
});
})
</script>
{% endblock %}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# #
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import re
import pyotp import pyotp
import base64 import base64
import logging import logging
...@@ -18,8 +19,11 @@ from django.core.cache import cache ...@@ -18,8 +19,11 @@ from django.core.cache import cache
from common.tasks import send_mail_async from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none from common.utils import reverse, get_object_or_none
from common.models import Setting
from common.forms import SecuritySettingForm
from .models import User, LoginLog from .models import User, LoginLog
logger = logging.getLogger('jumpserver') logger = logging.getLogger('jumpserver')
...@@ -271,3 +275,60 @@ def generate_otp_uri(request, issuer="Jumpserver"): ...@@ -271,3 +275,60 @@ def generate_otp_uri(request, issuer="Jumpserver"):
def check_otp_code(otp_secret_key, otp_code): def check_otp_code(otp_secret_key, otp_code):
totp = pyotp.TOTP(otp_secret_key) totp = pyotp.TOTP(otp_secret_key)
return totp.verify(otp_code) return totp.verify(otp_code)
def get_password_check_rules():
check_rules = []
min_length = settings.DEFAULT_PASSWORD_MIN_LENGTH
min_name = 'SECURITY_PASSWORD_MIN_LENGTH'
base_filed = SecuritySettingForm.base_fields
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
if not password_setting:
# 用户还没有设置过密码校验规则
label = base_filed.get(min_name).label
label += ' ' + str(min_length) + _('Bit')
id = 'rule_' + min_name
rules = {'id': id, 'label': label}
check_rules.append(rules)
for setting in password_setting:
if setting.cleaned_value:
id = 'rule_' + setting.name
label = base_filed.get(setting.name).label
if setting.name == min_name:
label += str(setting.cleaned_value) + _('Bit')
min_length = setting.cleaned_value
rules = {'id': id, 'label': label}
check_rules.append(rules)
return check_rules, min_length
def check_password_rules(password):
min_field_name = 'SECURITY_PASSWORD_MIN_LENGTH'
upper_field_name = 'SECURITY_PASSWORD_UPPER_CASE'
lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE'
number_field_name = 'SECURITY_PASSWORD_NUMBER'
special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR'
min_length_setting = Setting.objects.filter(name=min_field_name).first()
min_length = min_length_setting.value if min_length_setting else settings.DEFAULT_PASSWORD_MIN_LENGTH
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
if not password_setting:
pattern = r"^.{" + str(min_length) + ",}$"
else:
pattern = r"^"
for setting in password_setting:
if setting.cleaned_value and setting.name == upper_field_name:
pattern += '(?=.*[A-Z])'
elif setting.cleaned_value and setting.name == lower_field_name:
pattern += '(?=.*[a-z])'
elif setting.cleaned_value and setting.name == number_field_name:
pattern += '(?=.*\d)'
elif setting.cleaned_value and setting.name == special_field_name:
pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?])'
pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?]'
match_obj = re.match(pattern, password)
return bool(match_obj)
...@@ -23,9 +23,10 @@ from django.conf import settings ...@@ -23,9 +23,10 @@ from django.conf import settings
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
from common.models import Setting
from ..models import User, LoginLog from ..models import User, LoginLog
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \ from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \
get_user_or_tmp_user, set_tmp_user_to_cache get_user_or_tmp_user, set_tmp_user_to_cache, get_password_check_rules, check_password_rules
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
from .. import forms from .. import forms
...@@ -81,17 +82,24 @@ class UserLoginView(FormView): ...@@ -81,17 +82,24 @@ class UserLoginView(FormView):
def get_success_url(self): def get_success_url(self):
user = get_user_or_tmp_user(self.request) user = get_user_or_tmp_user(self.request)
if user.otp_enabled and user.otp_secret_key: mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
# 1,2 & T if mfa_setting and mfa_setting.cleaned_value:
return reverse('users:login-otp') if user.otp_enabled and user.otp_secret_key:
elif user.otp_enabled and not user.otp_secret_key: return reverse('users:login-otp')
# 1,2 & F else:
return reverse('users:user-otp-enable-authentication') return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled: else:
# 0 & T,F if user.otp_enabled and user.otp_secret_key:
auth_login(self.request, user) # 1,2 & T
self.write_login_log() return reverse('users:login-otp')
return redirect_user_first_login_or_index(self.request, self.redirect_field_name) elif user.otp_enabled and not user.otp_secret_key:
# 1,2 & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
self.write_login_log()
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -211,6 +219,10 @@ class UserResetPasswordView(TemplateView): ...@@ -211,6 +219,10 @@ class UserResetPasswordView(TemplateView):
token = request.GET.get('token') token = request.GET.get('token')
user = User.validate_reset_token(token) user = User.validate_reset_token(token)
check_rules, min_length = get_password_check_rules()
password_rules = {'password_check_rules': check_rules, 'min_length': min_length}
kwargs.update(password_rules)
if not user: if not user:
kwargs.update({'errors': _('Token invalid or expired')}) kwargs.update({'errors': _('Token invalid or expired')})
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
...@@ -227,6 +239,13 @@ class UserResetPasswordView(TemplateView): ...@@ -227,6 +239,13 @@ class UserResetPasswordView(TemplateView):
if not user: if not user:
return self.get(request, errors=_('Token invalid or expired')) return self.get(request, errors=_('Token invalid or expired'))
is_ok = check_password_rules(password)
if not is_ok:
return self.get(
request,
errors=_('* Your password does not meet the requirements')
)
user.reset_password(password) user.reset_password(password)
return HttpResponseRedirect(reverse('users:reset-password-success')) return HttpResponseRedirect(reverse('users:reset-password-success'))
......
...@@ -33,9 +33,10 @@ from django.contrib.auth import logout as auth_logout ...@@ -33,9 +33,10 @@ from django.contrib.auth import logout as auth_logout
from common.const import create_success_msg, update_success_msg from common.const import create_success_msg, update_success_msg
from common.mixins import JSONResponseMixin from common.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from common.models import Setting
from .. import forms from .. import forms
from ..models import User, UserGroup from ..models import User, UserGroup
from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user_or_tmp_user from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user_or_tmp_user, get_password_check_rules, check_password_rules
from ..signals import post_user_create from ..signals import post_user_create
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
...@@ -96,10 +97,27 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -96,10 +97,27 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = update_success_msg success_message = update_success_msg
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = {'app': _('Users'), 'action': _('Update user')} check_rules, min_length = get_password_check_rules()
context = {
'app': _('Users'),
'action': _('Update user'),
'password_check_rules': check_rules,
'min_length': min_length
}
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def form_valid(self, form):
password = form.cleaned_data.get('password')
is_ok = check_password_rules(password)
if not is_ok:
form.add_error(
"password", _("* Your password does not meet the requirements")
)
return self.form_invalid(form)
return super().form_valid(form)
class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView): class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView):
model = User model = User
...@@ -318,8 +336,11 @@ class UserProfileView(LoginRequiredMixin, TemplateView): ...@@ -318,8 +336,11 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
template_name = 'users/user_profile.html' template_name = 'users/user_profile.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
context = { context = {
'action': _('Profile'), 'action': _('Profile'),
'mfa_setting': mfa_setting.cleaned_value if mfa_setting else False,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -353,9 +374,12 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView): ...@@ -353,9 +374,12 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
return self.request.user return self.request.user
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
check_rules, min_length = get_password_check_rules()
context = { context = {
'app': _('Users'), 'app': _('Users'),
'action': _('Password update'), 'action': _('Password update'),
'password_check_rules': check_rules,
'min_length': min_length,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -364,6 +388,17 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView): ...@@ -364,6 +388,17 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
auth_logout(self.request) auth_logout(self.request)
return super().get_success_url() return super().get_success_url()
def form_valid(self, form):
password = form.cleaned_data.get('new_password')
is_ok = check_password_rules(password)
if not is_ok:
form.add_error(
"new_password",
_("* Your password does not meet the requirements")
)
return self.form_invalid(form)
return super().form_valid(form)
class UserPublicKeyUpdateView(LoginRequiredMixin, UpdateView): class UserPublicKeyUpdateView(LoginRequiredMixin, UpdateView):
template_name = 'users/user_pubkey_update.html' template_name = 'users/user_pubkey_update.html'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment