mirror of https://github.com/jumpserver/jumpserver
commit
e7a731fae9
|
@ -2,4 +2,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__version__ = "1.3.1"
|
||||
__version__ = "1.3.2"
|
||||
|
|
|
@ -116,7 +116,7 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
|||
def get_object(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
|
||||
if not pk:
|
||||
node = Node.root()
|
||||
node = None
|
||||
else:
|
||||
node = get_object_or_404(Node, pk=pk)
|
||||
return node
|
||||
|
@ -126,7 +126,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
|||
query_all = self.request.query_params.get("all")
|
||||
query_assets = self.request.query_params.get('assets')
|
||||
node = self.get_object()
|
||||
if node == Node.root():
|
||||
if node is None:
|
||||
node = Node.root()
|
||||
queryset.append(node)
|
||||
if query_all:
|
||||
children = node.get_all_children()
|
||||
|
|
|
@ -51,7 +51,6 @@ def test_gateway_connectability(gateway):
|
|||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
proxy = paramiko.SSHClient()
|
||||
proxy.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
|
||||
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
|
|
|
@ -168,3 +168,49 @@ 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"),
|
||||
min_value=6
|
||||
)
|
||||
# 大写字母
|
||||
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')
|
||||
)
|
||||
|
|
|
@ -34,7 +34,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
|
|||
def ldap_auth_on_changed(sender, enabled=True, **kwargs):
|
||||
if enabled:
|
||||
logger.debug("Enable LDAP auth")
|
||||
if settings.AUTH_LDAP_BACKEND not in settings.AUTH_LDAP_BACKEND:
|
||||
if settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
|
||||
settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND)
|
||||
|
||||
else:
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'settings:basic-setting' %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Basic setting' %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-12" style="padding-left:0">
|
||||
<div class="ibox-content" style="border-width: 0;padding-top: 40px;">
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
<h3>{% trans "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>
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -27,6 +27,9 @@
|
|||
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i
|
||||
class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
@ -39,6 +42,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
<h3>{% trans "Basic setting" %}</h3>
|
||||
{% for field in form %}
|
||||
{% if not field.field|is_bool_field %}
|
||||
|
@ -60,6 +64,7 @@
|
|||
{% endfor %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
|
||||
<h3>{% trans "Command storage" %}</h3>
|
||||
<table class="table table-hover " id="task-history-list-table">
|
||||
<thead>
|
||||
|
|
|
@ -11,4 +11,5 @@ urlpatterns = [
|
|||
url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'),
|
||||
url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'),
|
||||
url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'),
|
||||
url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'),
|
||||
]
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _
|
|||
from django.conf import settings
|
||||
|
||||
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
|
||||
TerminalSettingForm
|
||||
TerminalSettingForm, SecuritySettingForm
|
||||
from .mixins import AdminUserRequiredMixin
|
||||
from .signals import ldap_auth_enable
|
||||
|
||||
|
@ -82,7 +82,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
|
|||
if form.is_valid():
|
||||
form.save()
|
||||
if "AUTH_LDAP" in form.cleaned_data:
|
||||
ldap_auth_enable.send(form.cleaned_data["AUTH_LDAP"])
|
||||
ldap_auth_enable.send(sender=self.__class__, enabled=form.cleaned_data["AUTH_LDAP"])
|
||||
msg = _("Update setting successfully, please restart program")
|
||||
messages.success(request, msg)
|
||||
return redirect('settings:ldap-setting')
|
||||
|
@ -122,3 +122,27 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class SecuritySettingView(AdminUserRequiredMixin, TemplateView):
|
||||
form_class = SecuritySettingForm
|
||||
template_name = "common/security_setting.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Settings'),
|
||||
'action': _('Security setting'),
|
||||
'form': self.form_class(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def post(self, request):
|
||||
form = self.form_class(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
msg = _("Update setting successfully, please restart program")
|
||||
messages.success(request, msg)
|
||||
return redirect('settings:security-setting')
|
||||
else:
|
||||
context = self.get_context_data()
|
||||
context.update({"form": form})
|
||||
return render(request, self.template_name, context)
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
BOOTSTRAP3 = {
|
||||
'horizontal_label_class': 'col-md-2',
|
||||
|
|
|
@ -86,6 +86,7 @@ class JMSInventory(BaseInventory):
|
|||
gateway = asset.domain.random_gateway()
|
||||
proxy_command_list = [
|
||||
"ssh", "-p", str(gateway.port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"{}@{}".format(gateway.username, gateway.ip),
|
||||
"-W", "%h:%p", "-q",
|
||||
]
|
||||
|
|
|
@ -609,3 +609,91 @@ function setUrlParam(url, name, value) {
|
|||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// 校验密码-改变规则颜色
|
||||
function checkPasswordRules(password, minLength) {
|
||||
if (wordMinLength(password, minLength)) {
|
||||
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordUpperCase(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', 'green');
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordLowerCase(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordNumber(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', '#908a8a')
|
||||
}
|
||||
|
||||
if (wordSpecialChar(password)) {
|
||||
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', 'green')
|
||||
}
|
||||
else {
|
||||
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', '#908a8a')
|
||||
}
|
||||
}
|
||||
|
||||
// 最小长度
|
||||
function wordMinLength(word, minLength) {
|
||||
//var minLength = {{ min_length }};
|
||||
var re = new RegExp("^(.{" + minLength + ",})$");
|
||||
return word.match(re)
|
||||
}
|
||||
// 大写字母
|
||||
function wordUpperCase(word) {
|
||||
return word.match(/([A-Z]+)/)
|
||||
}
|
||||
// 小写字母
|
||||
function wordLowerCase(word) {
|
||||
return word.match(/([a-z]+)/)
|
||||
}
|
||||
// 数字字符
|
||||
function wordNumber(word) {
|
||||
return word.match(/([\d]+)/)
|
||||
}
|
||||
// 特殊字符
|
||||
function wordSpecialChar(word) {
|
||||
return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/)
|
||||
}
|
||||
|
||||
// 显示弹窗密码规则
|
||||
function popoverPasswordRules(password_check_rules, $el) {
|
||||
var message = "";
|
||||
jQuery.each(password_check_rules, function (idx, rules) {
|
||||
message += "<li id=" + rules.id + " style='list-style-type:none;'> <i class='fa fa-check-circle-o' style='margin-right:10px;' ></i>" + rules.label + "</li>";
|
||||
});
|
||||
//$('#id_password_rules').html(message);
|
||||
$el.html(message)
|
||||
}
|
||||
|
||||
// 初始化弹窗popover
|
||||
function initPopover($container, $progress, $idPassword, $el, password_check_rules){
|
||||
options = {};
|
||||
// User Interface
|
||||
options.ui = {
|
||||
container: $container,
|
||||
viewports: {
|
||||
progress: $progress
|
||||
//errors: $('.popover-content')
|
||||
},
|
||||
showProgressbar: true,
|
||||
showVerdictsInsideProgressBar: true
|
||||
};
|
||||
$idPassword.pwstrength(options);
|
||||
popoverPasswordRules(password_check_rules, $el);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,976 @@
|
|||
/*!
|
||||
* jQuery Password Strength plugin for Twitter Bootstrap
|
||||
* Version: 2.2.1
|
||||
*
|
||||
* Copyright (c) 2008-2013 Tane Piper
|
||||
* Copyright (c) 2013 Alejandro Blanco
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
|
||||
(function (jQuery) {
|
||||
// Source: src/i18n.js
|
||||
|
||||
|
||||
|
||||
|
||||
var i18n = {};
|
||||
|
||||
(function (i18n, i18next) {
|
||||
'use strict';
|
||||
|
||||
i18n.fallback = {
|
||||
"wordMinLength": "Your password is too short",
|
||||
"wordMaxLength": "Your password is too long",
|
||||
"wordInvalidChar": "Your password contains an invalid character",
|
||||
"wordNotEmail": "Do not use your email as your password",
|
||||
"wordSimilarToUsername": "Your password cannot contain your username",
|
||||
"wordTwoCharacterClasses": "Use different character classes",
|
||||
"wordRepetitions": "Too many repetitions",
|
||||
"wordSequences": "Your password contains sequences",
|
||||
"errorList": "Errors:",
|
||||
"veryWeak": "Very Weak",
|
||||
"weak": "Weak",
|
||||
"normal": "Normal",
|
||||
"medium": "Medium",
|
||||
"strong": "Strong",
|
||||
"veryStrong": "Very Strong"
|
||||
};
|
||||
|
||||
i18n.t = function (key) {
|
||||
var result = '';
|
||||
|
||||
// Try to use i18next.com
|
||||
if (i18next) {
|
||||
result = i18next.t(key);
|
||||
} else {
|
||||
// Fallback to english
|
||||
result = i18n.fallback[key];
|
||||
}
|
||||
|
||||
return result === key ? '' : result;
|
||||
};
|
||||
}(i18n, window.i18next));
|
||||
|
||||
// Source: src/rules.js
|
||||
|
||||
|
||||
|
||||
|
||||
var rulesEngine = {};
|
||||
|
||||
try {
|
||||
if (!jQuery && module && module.exports) {
|
||||
var jQuery = require("jquery"),
|
||||
jsdom = require("jsdom").jsdom;
|
||||
jQuery = jQuery(jsdom().defaultView);
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
(function ($, rulesEngine) {
|
||||
"use strict";
|
||||
var validation = {};
|
||||
|
||||
rulesEngine.forbiddenSequences = [
|
||||
"0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl",
|
||||
"zxcvbnm", "!@#$%^&*()_+"
|
||||
];
|
||||
|
||||
validation.wordNotEmail = function (options, word, score) {
|
||||
if (word.match(/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordMinLength = function (options, word, score) {
|
||||
var wordlen = word.length,
|
||||
lenScore = Math.pow(wordlen, options.rules.raisePower);
|
||||
if (wordlen < options.common.minChar) {
|
||||
lenScore = (lenScore + score);
|
||||
}
|
||||
return lenScore;
|
||||
};
|
||||
|
||||
validation.wordMaxLength = function (options, word, score) {
|
||||
var wordlen = word.length,
|
||||
lenScore = Math.pow(wordlen, options.rules.raisePower);
|
||||
if (wordlen > options.common.maxChar) {
|
||||
return score;
|
||||
}
|
||||
return lenScore;
|
||||
};
|
||||
|
||||
validation.wordInvalidChar = function (options, word, score) {
|
||||
if (options.common.invalidCharsRegExp.test(word)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordMinLengthStaticScore = function (options, word, score) {
|
||||
return word.length < options.common.minChar ? 0 : score;
|
||||
};
|
||||
|
||||
validation.wordMaxLengthStaticScore = function (options, word, score) {
|
||||
return word.length > options.common.maxChar ? 0 : score;
|
||||
};
|
||||
|
||||
|
||||
validation.wordSimilarToUsername = function (options, word, score) {
|
||||
var username = $(options.common.usernameField).val();
|
||||
if (username && word.toLowerCase().match(username.replace(/[\-\[\]\/\{\}\(\)\*\+\=\?\:\.\\\^\$\|\!\,]/g, "\\$&").toLowerCase())) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordTwoCharacterClasses = function (options, word, score) {
|
||||
if (word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) ||
|
||||
(word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) ||
|
||||
(word.match(/(.[!,@,#,$,%,\^,&,*,?,_,~])/) && word.match(/[a-zA-Z0-9_]/))) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordRepetitions = function (options, word, score) {
|
||||
if (word.match(/(.)\1\1/)) { return score; }
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordSequences = function (options, word, score) {
|
||||
var found = false,
|
||||
j;
|
||||
if (word.length > 2) {
|
||||
$.each(rulesEngine.forbiddenSequences, function (idx, seq) {
|
||||
if (found) { return; }
|
||||
var sequences = [seq, seq.split('').reverse().join('')];
|
||||
$.each(sequences, function (idx, sequence) {
|
||||
for (j = 0; j < (word.length - 2); j += 1) { // iterate the word trough a sliding window of size 3:
|
||||
if (sequence.indexOf(word.toLowerCase().substring(j, j + 3)) > -1) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (found) { return score; }
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordLowercase = function (options, word, score) {
|
||||
return word.match(/[a-z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordUppercase = function (options, word, score) {
|
||||
return word.match(/[A-Z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneNumber = function (options, word, score) {
|
||||
return word.match(/\d+/) && score;
|
||||
};
|
||||
|
||||
validation.wordThreeNumbers = function (options, word, score) {
|
||||
return word.match(/(.*[0-9].*[0-9].*[0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneSpecialChar = function (options, word, score) {
|
||||
return word.match(/[!,@,#,$,%,\^,&,*,?,_,~]/) && score;
|
||||
};
|
||||
|
||||
validation.wordTwoSpecialChar = function (options, word, score) {
|
||||
return word.match(/(.*[!,@,#,$,%,\^,&,*,?,_,~].*[!,@,#,$,%,\^,&,*,?,_,~])/) && score;
|
||||
};
|
||||
|
||||
validation.wordUpperLowerCombo = function (options, word, score) {
|
||||
return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCombo = function (options, word, score) {
|
||||
return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCharCombo = function (options, word, score) {
|
||||
return word.match(/([a-zA-Z0-9].*[!,@,#,$,%,\^,&,*,?,_,~])|([!,@,#,$,%,\^,&,*,?,_,~].*[a-zA-Z0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordIsACommonPassword = function (options, word, score) {
|
||||
if ($.inArray(word, options.rules.commonPasswords) >= 0) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
rulesEngine.validation = validation;
|
||||
|
||||
rulesEngine.executeRules = function (options, word) {
|
||||
var totalScore = 0;
|
||||
|
||||
$.each(options.rules.activated, function (rule, active) {
|
||||
if (active) {
|
||||
var score = options.rules.scores[rule],
|
||||
funct = rulesEngine.validation[rule],
|
||||
result,
|
||||
errorMessage;
|
||||
|
||||
if (!$.isFunction(funct)) {
|
||||
funct = options.rules.extra[rule];
|
||||
}
|
||||
|
||||
if ($.isFunction(funct)) {
|
||||
result = funct(options, word, score);
|
||||
if (result) {
|
||||
totalScore += result;
|
||||
}
|
||||
if (result < 0 || (!$.isNumeric(result) && !result)) {
|
||||
errorMessage = options.ui.spanError(options, rule);
|
||||
if (errorMessage.length > 0) {
|
||||
options.instances.errors.push(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return totalScore;
|
||||
};
|
||||
}(jQuery, rulesEngine));
|
||||
|
||||
try {
|
||||
if (module && module.exports) {
|
||||
module.exports = rulesEngine;
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
// Source: src/options.js
|
||||
|
||||
|
||||
|
||||
|
||||
var defaultOptions = {};
|
||||
|
||||
defaultOptions.common = {};
|
||||
defaultOptions.common.minChar = 6;
|
||||
defaultOptions.common.maxChar = 20;
|
||||
defaultOptions.common.usernameField = "#username";
|
||||
defaultOptions.common.invalidCharsRegExp = new RegExp(/[\s,'"]/);
|
||||
defaultOptions.common.userInputs = [
|
||||
// Selectors for input fields with user input
|
||||
];
|
||||
defaultOptions.common.onLoad = undefined;
|
||||
defaultOptions.common.onKeyUp = undefined;
|
||||
defaultOptions.common.onScore = undefined;
|
||||
defaultOptions.common.zxcvbn = false;
|
||||
defaultOptions.common.zxcvbnTerms = [
|
||||
// List of disrecommended words
|
||||
];
|
||||
defaultOptions.common.events = ["keyup", "change", "paste"];
|
||||
defaultOptions.common.debug = false;
|
||||
|
||||
defaultOptions.rules = {};
|
||||
defaultOptions.rules.extra = {};
|
||||
defaultOptions.rules.scores = {
|
||||
wordNotEmail: -100,
|
||||
wordMinLength: -50,
|
||||
wordMaxLength: -50,
|
||||
wordInvalidChar: -100,
|
||||
wordSimilarToUsername: -100,
|
||||
wordSequences: -20,
|
||||
wordTwoCharacterClasses: 2,
|
||||
wordRepetitions: -25,
|
||||
wordLowercase: 1,
|
||||
wordUppercase: 3,
|
||||
wordOneNumber: 3,
|
||||
wordThreeNumbers: 5,
|
||||
wordOneSpecialChar: 3,
|
||||
wordTwoSpecialChar: 5,
|
||||
wordUpperLowerCombo: 2,
|
||||
wordLetterNumberCombo: 2,
|
||||
wordLetterNumberCharCombo: 2,
|
||||
wordIsACommonPassword: -100
|
||||
};
|
||||
defaultOptions.rules.activated = {
|
||||
wordNotEmail: true,
|
||||
wordMinLength: true,
|
||||
wordMaxLength: false,
|
||||
wordInvalidChar: false,
|
||||
wordSimilarToUsername: true,
|
||||
wordSequences: true,
|
||||
wordTwoCharacterClasses: true,
|
||||
wordRepetitions: true,
|
||||
wordLowercase: true,
|
||||
wordUppercase: true,
|
||||
wordOneNumber: true,
|
||||
wordThreeNumbers: true,
|
||||
wordOneSpecialChar: true,
|
||||
wordTwoSpecialChar: true,
|
||||
wordUpperLowerCombo: true,
|
||||
wordLetterNumberCombo: true,
|
||||
wordLetterNumberCharCombo: true,
|
||||
wordIsACommonPassword: true
|
||||
};
|
||||
defaultOptions.rules.raisePower = 1.4;
|
||||
// List taken from https://github.com/danielmiessler/SecLists (MIT License)
|
||||
defaultOptions.rules.commonPasswords = [
|
||||
'123456',
|
||||
'password',
|
||||
'12345678',
|
||||
'qwerty',
|
||||
'123456789',
|
||||
'12345',
|
||||
'1234',
|
||||
'111111',
|
||||
'1234567',
|
||||
'dragon',
|
||||
'123123',
|
||||
'baseball',
|
||||
'abc123',
|
||||
'football',
|
||||
'monkey',
|
||||
'letmein',
|
||||
'696969',
|
||||
'shadow',
|
||||
'master',
|
||||
'666666',
|
||||
'qwertyuiop',
|
||||
'123321',
|
||||
'mustang',
|
||||
'1234567890',
|
||||
'michael',
|
||||
'654321',
|
||||
'pussy',
|
||||
'superman',
|
||||
'1qaz2wsx',
|
||||
'7777777',
|
||||
'fuckyou',
|
||||
'121212',
|
||||
'000000',
|
||||
'qazwsx',
|
||||
'123qwe',
|
||||
'killer',
|
||||
'trustno1',
|
||||
'jordan',
|
||||
'jennifer',
|
||||
'zxcvbnm',
|
||||
'asdfgh',
|
||||
'hunter',
|
||||
'buster',
|
||||
'soccer',
|
||||
'harley',
|
||||
'batman',
|
||||
'andrew',
|
||||
'tigger',
|
||||
'sunshine',
|
||||
'iloveyou',
|
||||
'fuckme',
|
||||
'2000',
|
||||
'charlie',
|
||||
'robert',
|
||||
'thomas',
|
||||
'hockey',
|
||||
'ranger',
|
||||
'daniel',
|
||||
'starwars',
|
||||
'klaster',
|
||||
'112233',
|
||||
'george',
|
||||
'asshole',
|
||||
'computer',
|
||||
'michelle',
|
||||
'jessica',
|
||||
'pepper',
|
||||
'1111',
|
||||
'zxcvbn',
|
||||
'555555',
|
||||
'11111111',
|
||||
'131313',
|
||||
'freedom',
|
||||
'777777',
|
||||
'pass',
|
||||
'fuck',
|
||||
'maggie',
|
||||
'159753',
|
||||
'aaaaaa',
|
||||
'ginger',
|
||||
'princess',
|
||||
'joshua',
|
||||
'cheese',
|
||||
'amanda',
|
||||
'summer',
|
||||
'love',
|
||||
'ashley',
|
||||
'6969',
|
||||
'nicole',
|
||||
'chelsea',
|
||||
'biteme',
|
||||
'matthew',
|
||||
'access',
|
||||
'yankees',
|
||||
'987654321',
|
||||
'dallas',
|
||||
'austin',
|
||||
'thunder',
|
||||
'taylor',
|
||||
'matrix'
|
||||
];
|
||||
|
||||
defaultOptions.ui = {};
|
||||
defaultOptions.ui.bootstrap2 = false;
|
||||
defaultOptions.ui.bootstrap4 = false;
|
||||
defaultOptions.ui.colorClasses = [
|
||||
"danger", "danger", "danger", "warning", "warning", "success"
|
||||
];
|
||||
defaultOptions.ui.showProgressBar = true;
|
||||
defaultOptions.ui.progressBarEmptyPercentage = 1;
|
||||
defaultOptions.ui.progressBarMinPercentage = 1;
|
||||
defaultOptions.ui.progressExtraCssClasses = '';
|
||||
defaultOptions.ui.progressBarExtraCssClasses = '';
|
||||
defaultOptions.ui.showPopover = false;
|
||||
defaultOptions.ui.popoverPlacement = "bottom";
|
||||
defaultOptions.ui.showStatus = false;
|
||||
defaultOptions.ui.spanError = function (options, key) {
|
||||
"use strict";
|
||||
var text = options.i18n.t(key);
|
||||
if (!text) { return ''; }
|
||||
return '<span style="color: #d52929">' + text + '</span>';
|
||||
};
|
||||
defaultOptions.ui.popoverError = function (options) {
|
||||
"use strict";
|
||||
var errors = options.instances.errors,
|
||||
errorsTitle = options.i18n.t("errorList"),
|
||||
message = "<div>" + errorsTitle + "<ul class='error-list' style='margin-bottom: 0;'>";
|
||||
|
||||
jQuery.each(errors, function (idx, err) {
|
||||
message += "<li>" + err + "</li>";
|
||||
});
|
||||
message += "</ul></div>";
|
||||
return message;
|
||||
};
|
||||
defaultOptions.ui.showVerdicts = true;
|
||||
defaultOptions.ui.showVerdictsInsideProgressBar = false;
|
||||
defaultOptions.ui.useVerdictCssClass = false;
|
||||
defaultOptions.ui.showErrors = false;
|
||||
defaultOptions.ui.showScore = false;
|
||||
defaultOptions.ui.container = undefined;
|
||||
defaultOptions.ui.viewports = {
|
||||
progress: undefined,
|
||||
verdict: undefined,
|
||||
errors: undefined,
|
||||
score: undefined
|
||||
};
|
||||
defaultOptions.ui.scores = [0, 14, 26, 38, 50];
|
||||
|
||||
defaultOptions.i18n = {};
|
||||
defaultOptions.i18n.t = i18n.t;
|
||||
|
||||
// Source: src/ui.js
|
||||
|
||||
|
||||
|
||||
|
||||
var ui = {};
|
||||
|
||||
(function ($, ui) {
|
||||
"use strict";
|
||||
|
||||
var statusClasses = ["error", "warning", "success"],
|
||||
verdictKeys = [
|
||||
"veryWeak", "weak", "normal", "medium", "strong", "veryStrong"
|
||||
];
|
||||
|
||||
ui.getContainer = function (options, $el) {
|
||||
var $container;
|
||||
|
||||
$container = $(options.ui.container);
|
||||
if (!($container && $container.length === 1)) {
|
||||
$container = $el.parent();
|
||||
}
|
||||
return $container;
|
||||
};
|
||||
|
||||
ui.findElement = function ($container, viewport, cssSelector) {
|
||||
if (viewport) {
|
||||
return $container.find(viewport).find(cssSelector);
|
||||
}
|
||||
return $container.find(cssSelector);
|
||||
};
|
||||
|
||||
ui.getUIElements = function (options, $el) {
|
||||
var $container, result;
|
||||
|
||||
if (options.instances.viewports) {
|
||||
return options.instances.viewports;
|
||||
}
|
||||
|
||||
$container = ui.getContainer(options, $el);
|
||||
|
||||
result = {};
|
||||
result.$progressbar = ui.findElement($container, options.ui.viewports.progress, "div.progress");
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = result.$progressbar.find("span.password-verdict");
|
||||
}
|
||||
|
||||
if (!options.ui.showPopover) {
|
||||
if (!options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = ui.findElement($container, options.ui.viewports.verdict, "span.password-verdict");
|
||||
}
|
||||
result.$errors = ui.findElement($container, options.ui.viewports.errors, "ul.error-list");
|
||||
}
|
||||
result.$score = ui.findElement($container, options.ui.viewports.score,
|
||||
"span.password-score");
|
||||
|
||||
options.instances.viewports = result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.initProgressBar = function (options, $el) {
|
||||
var $container = ui.getContainer(options, $el),
|
||||
progressbar = "<div class='progress ";
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
// Boostrap 2
|
||||
progressbar += options.ui.progressBarExtraCssClasses +
|
||||
"'><div class='";
|
||||
} else {
|
||||
// Bootstrap 3 & 4
|
||||
progressbar += options.ui.progressExtraCssClasses + "'><div class='" +
|
||||
options.ui.progressBarExtraCssClasses + " progress-";
|
||||
}
|
||||
progressbar += "bar'>";
|
||||
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
progressbar += "<span class='password-verdict'></span>";
|
||||
}
|
||||
|
||||
progressbar += "</div></div>";
|
||||
|
||||
if (options.ui.viewports.progress) {
|
||||
$container.find(options.ui.viewports.progress).append(progressbar);
|
||||
} else {
|
||||
$(progressbar).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initHelper = function (options, $el, html, viewport) {
|
||||
var $container = ui.getContainer(options, $el);
|
||||
if (viewport) {
|
||||
$container.find(viewport).append(html);
|
||||
} else {
|
||||
$(html).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initVerdict = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<span class='password-verdict'></span>",
|
||||
options.ui.viewports.verdict);
|
||||
};
|
||||
|
||||
ui.initErrorList = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<ul class='error-list'></ul >",
|
||||
options.ui.viewports.errors);
|
||||
};
|
||||
|
||||
ui.initScore = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<span class='password-score'></span>",
|
||||
options.ui.viewports.score);
|
||||
};
|
||||
|
||||
ui.initPopover = function (options, $el) {
|
||||
$el.popover("destroy");
|
||||
$el.popover({
|
||||
html: true,
|
||||
placement: options.ui.popoverPlacement,
|
||||
trigger: "manual",
|
||||
content: " "
|
||||
});
|
||||
};
|
||||
|
||||
ui.initUI = function (options, $el) {
|
||||
if (options.ui.showPopover) {
|
||||
ui.initPopover(options, $el);
|
||||
} else {
|
||||
if (options.ui.showErrors) { ui.initErrorList(options, $el); }
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.initVerdict(options, $el);
|
||||
}
|
||||
}
|
||||
if (options.ui.showProgressBar) {
|
||||
ui.initProgressBar(options, $el);
|
||||
}
|
||||
if (options.ui.showScore) {
|
||||
ui.initScore(options, $el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateProgressBar = function (options, $el, cssClass, percentage) {
|
||||
var $progressbar = ui.getUIElements(options, $el).$progressbar,
|
||||
$bar = $progressbar.find(".progress-bar"),
|
||||
cssPrefix = "progress-";
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
$bar = $progressbar.find(".bar");
|
||||
cssPrefix = "";
|
||||
}
|
||||
|
||||
$.each(options.ui.colorClasses, function (idx, value) {
|
||||
if (options.ui.bootstrap4) {
|
||||
$bar.removeClass("bg-" + value);
|
||||
} else {
|
||||
$bar.removeClass(cssPrefix + "bar-" + value);
|
||||
}
|
||||
});
|
||||
if (options.ui.bootstrap4) {
|
||||
$bar.addClass("bg-" + options.ui.colorClasses[cssClass]);
|
||||
} else {
|
||||
$bar.addClass(cssPrefix + "bar-" + options.ui.colorClasses[cssClass]);
|
||||
}
|
||||
$bar.css("width", percentage + '%');
|
||||
};
|
||||
|
||||
ui.updateVerdict = function (options, $el, cssClass, text) {
|
||||
var $verdict = ui.getUIElements(options, $el).$verdict;
|
||||
$verdict.removeClass(options.ui.colorClasses.join(' '));
|
||||
if (cssClass > -1) {
|
||||
$verdict.addClass(options.ui.colorClasses[cssClass]);
|
||||
}
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
$verdict.css('white-space', 'nowrap');
|
||||
}
|
||||
$verdict.html(text);
|
||||
};
|
||||
|
||||
ui.updateErrors = function (options, $el, remove) {
|
||||
var $errors = ui.getUIElements(options, $el).$errors,
|
||||
html = "";
|
||||
|
||||
if (!remove) {
|
||||
$.each(options.instances.errors, function (idx, err) {
|
||||
html += "<li style='list-style-type:none;'>" + err + "</li>";
|
||||
});
|
||||
}
|
||||
$errors.html(html);
|
||||
};
|
||||
|
||||
ui.updateScore = function (options, $el, score, remove) {
|
||||
var $score = ui.getUIElements(options, $el).$score,
|
||||
html = "";
|
||||
|
||||
if (!remove) { html = score.toFixed(2); }
|
||||
$score.html(html);
|
||||
};
|
||||
|
||||
ui.updatePopover = function (options, $el, verdictText, remove) {
|
||||
var popover = $el.data("bs.popover"),
|
||||
html = "",
|
||||
hide = true;
|
||||
|
||||
if (options.ui.showVerdicts &&
|
||||
!options.ui.showVerdictsInsideProgressBar &&
|
||||
verdictText.length > 0) {
|
||||
html = "<h5><span class='password-verdict'>" + verdictText +
|
||||
"</span></h5>";
|
||||
hide = false;
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
if (options.instances.errors.length > 0) {
|
||||
hide = false;
|
||||
}
|
||||
html += options.ui.popoverError(options);
|
||||
}
|
||||
|
||||
if (hide || remove) {
|
||||
$el.popover("hide");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.ui.bootstrap2) { popover = $el.data("popover"); }
|
||||
|
||||
if (popover.$arrow && popover.$arrow.parents("body").length > 0) {
|
||||
$el.find("+ .popover .popover-content").html(html);
|
||||
} else {
|
||||
// It's hidden
|
||||
popover.options.content = html;
|
||||
$el.popover("show");
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateFieldStatus = function (options, $el, cssClass, remove) {
|
||||
var targetClass = options.ui.bootstrap2 ? ".control-group" : ".form-group",
|
||||
$container = $el.parents(targetClass).first();
|
||||
|
||||
$.each(statusClasses, function (idx, css) {
|
||||
if (!options.ui.bootstrap2) { css = "has-" + css; }
|
||||
$container.removeClass(css);
|
||||
});
|
||||
|
||||
if (remove) { return; }
|
||||
|
||||
cssClass = statusClasses[Math.floor(cssClass / 2)];
|
||||
if (!options.ui.bootstrap2) { cssClass = "has-" + cssClass; }
|
||||
$container.addClass(cssClass);
|
||||
};
|
||||
|
||||
ui.percentage = function (options, score, maximun) {
|
||||
var result = Math.floor(100 * score / maximun),
|
||||
min = options.ui.progressBarMinPercentage;
|
||||
|
||||
result = result <= min ? min : result;
|
||||
result = result > 100 ? 100 : result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.getVerdictAndCssClass = function (options, score) {
|
||||
var level, verdict;
|
||||
|
||||
if (score === undefined) { return ['', 0]; }
|
||||
|
||||
if (score <= options.ui.scores[0]) {
|
||||
level = 0;
|
||||
} else if (score < options.ui.scores[1]) {
|
||||
level = 1;
|
||||
} else if (score < options.ui.scores[2]) {
|
||||
level = 2;
|
||||
} else if (score < options.ui.scores[3]) {
|
||||
level = 3;
|
||||
} else if (score < options.ui.scores[4]) {
|
||||
level = 4;
|
||||
} else {
|
||||
level = 5;
|
||||
}
|
||||
|
||||
verdict = verdictKeys[level];
|
||||
|
||||
return [options.i18n.t(verdict), level];
|
||||
};
|
||||
|
||||
ui.updateUI = function (options, $el, score) {
|
||||
var cssClass, barPercentage, verdictText, verdictCssClass;
|
||||
|
||||
cssClass = ui.getVerdictAndCssClass(options, score);
|
||||
verdictText = score === 0 ? '' : cssClass[0];
|
||||
cssClass = cssClass[1];
|
||||
verdictCssClass = options.ui.useVerdictCssClass ? cssClass : -1;
|
||||
|
||||
if (options.ui.showProgressBar) {
|
||||
if (score === undefined) {
|
||||
barPercentage = options.ui.progressBarEmptyPercentage;
|
||||
} else {
|
||||
barPercentage = ui.percentage(options, score, options.ui.scores[4]);
|
||||
}
|
||||
ui.updateProgressBar(options, $el, cssClass, barPercentage);
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ui.showStatus) {
|
||||
ui.updateFieldStatus(options, $el, cssClass, score === undefined);
|
||||
}
|
||||
|
||||
if (options.ui.showPopover) {
|
||||
ui.updatePopover(options, $el, verdictText, score === undefined);
|
||||
} else {
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
ui.updateErrors(options, $el, score === undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ui.showScore) {
|
||||
ui.updateScore(options, $el, score, score === undefined);
|
||||
}
|
||||
};
|
||||
}(jQuery, ui));
|
||||
|
||||
// Source: src/methods.js
|
||||
|
||||
|
||||
|
||||
|
||||
var methods = {};
|
||||
|
||||
(function ($, methods) {
|
||||
"use strict";
|
||||
var onKeyUp, onPaste, applyToAll;
|
||||
|
||||
onKeyUp = function (event) {
|
||||
var $el = $(event.target),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
word = $el.val(),
|
||||
userInputs,
|
||||
verdictText,
|
||||
verdictLevel,
|
||||
score;
|
||||
|
||||
if (options === undefined) { return; }
|
||||
|
||||
options.instances.errors = [];
|
||||
if (word.length === 0) {
|
||||
score = undefined;
|
||||
} else {
|
||||
if (options.common.zxcvbn) {
|
||||
userInputs = [];
|
||||
$.each(options.common.userInputs.concat([options.common.usernameField]), function (idx, selector) {
|
||||
var value = $(selector).val();
|
||||
if (value) { userInputs.push(value); }
|
||||
});
|
||||
userInputs = userInputs.concat(options.common.zxcvbnTerms);
|
||||
score = zxcvbn(word, userInputs).guesses;
|
||||
score = Math.log(score) * Math.LOG2E;
|
||||
} else {
|
||||
score = rulesEngine.executeRules(options, word);
|
||||
}
|
||||
if ($.isFunction(options.common.onScore)) {
|
||||
score = options.common.onScore(options, word, score);
|
||||
}
|
||||
}
|
||||
ui.updateUI(options, $el, score);
|
||||
verdictText = ui.getVerdictAndCssClass(options, score);
|
||||
verdictLevel = verdictText[1];
|
||||
verdictText = verdictText[0];
|
||||
|
||||
if (options.common.debug) {
|
||||
console.log(score + ' - ' + verdictText);
|
||||
}
|
||||
|
||||
if ($.isFunction(options.common.onKeyUp)) {
|
||||
options.common.onKeyUp(event, {
|
||||
score: score,
|
||||
verdictText: verdictText,
|
||||
verdictLevel: verdictLevel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPaste = function (event) {
|
||||
// This handler is necessary because the paste event fires before the
|
||||
// content is actually in the input, so we cannot read its value right
|
||||
// away. Therefore, the timeouts.
|
||||
var $el = $(event.target),
|
||||
word = $el.val(),
|
||||
tries = 0,
|
||||
callback;
|
||||
|
||||
callback = function () {
|
||||
var newWord = $el.val();
|
||||
|
||||
if (newWord !== word) {
|
||||
onKeyUp(event);
|
||||
} else if (tries < 3) {
|
||||
tries += 1;
|
||||
setTimeout(callback, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(callback, 100);
|
||||
};
|
||||
|
||||
methods.init = function (settings) {
|
||||
this.each(function (idx, el) {
|
||||
// Make it deep extend (first param) so it extends also the
|
||||
// rules and other inside objects
|
||||
var clonedDefaults = $.extend(true, {}, defaultOptions),
|
||||
localOptions = $.extend(true, clonedDefaults, settings),
|
||||
$el = $(el);
|
||||
|
||||
localOptions.instances = {};
|
||||
$el.data("pwstrength-bootstrap", localOptions);
|
||||
|
||||
$.each(localOptions.common.events, function (idx, eventName) {
|
||||
var handler = eventName === "paste" ? onPaste : onKeyUp;
|
||||
$el.on(eventName, handler);
|
||||
});
|
||||
|
||||
ui.initUI(localOptions, $el);
|
||||
$el.trigger("keyup");
|
||||
|
||||
if ($.isFunction(localOptions.common.onLoad)) {
|
||||
localOptions.common.onLoad();
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
methods.destroy = function () {
|
||||
this.each(function (idx, el) {
|
||||
var $el = $(el),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
elements = ui.getUIElements(options, $el);
|
||||
elements.$progressbar.remove();
|
||||
elements.$verdict.remove();
|
||||
elements.$errors.remove();
|
||||
$el.removeData("pwstrength-bootstrap");
|
||||
});
|
||||
};
|
||||
|
||||
methods.forceUpdate = function () {
|
||||
this.each(function (idx, el) {
|
||||
var event = { target: el };
|
||||
onKeyUp(event);
|
||||
});
|
||||
};
|
||||
|
||||
methods.addRule = function (name, method, score, active) {
|
||||
this.each(function (idx, el) {
|
||||
var options = $(el).data("pwstrength-bootstrap");
|
||||
|
||||
options.rules.activated[name] = active;
|
||||
options.rules.scores[name] = score;
|
||||
options.rules.extra[name] = method;
|
||||
});
|
||||
};
|
||||
|
||||
applyToAll = function (rule, prop, value) {
|
||||
this.each(function (idx, el) {
|
||||
$(el).data("pwstrength-bootstrap").rules[prop][rule] = value;
|
||||
});
|
||||
};
|
||||
|
||||
methods.changeScore = function (rule, score) {
|
||||
applyToAll.call(this, rule, "scores", score);
|
||||
};
|
||||
|
||||
methods.ruleActive = function (rule, active) {
|
||||
applyToAll.call(this, rule, "activated", active);
|
||||
};
|
||||
|
||||
methods.ruleIsMet = function (rule) {
|
||||
if ($.isFunction(rulesEngine.validation[rule])) {
|
||||
if (rule === "wordMinLength") {
|
||||
rule = "wordMinLengthStaticScore";
|
||||
} else if (rule === "wordMaxLength") {
|
||||
rule = "wordMaxLengthStaticScore";
|
||||
}
|
||||
|
||||
var rulesMetCnt = 0;
|
||||
|
||||
this.each(function (idx, el) {
|
||||
var options = $(el).data("pwstrength-bootstrap");
|
||||
|
||||
rulesMetCnt += rulesEngine.validation[rule](options, $(el).val(), 1);
|
||||
});
|
||||
|
||||
return (rulesMetCnt === this.length);
|
||||
}
|
||||
|
||||
$.error("Rule " + rule + " does not exist on jQuery.pwstrength-bootstrap.validation");
|
||||
};
|
||||
|
||||
$.fn.pwstrength = function (method) {
|
||||
var result;
|
||||
|
||||
if (methods[method]) {
|
||||
result = methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
} else if (typeof method === "object" || !method) {
|
||||
result = methods.init.apply(this, arguments);
|
||||
} else {
|
||||
$.error("Method " + method + " does not exist on jQuery.pwstrength-bootstrap");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}(jQuery, methods));
|
||||
}(jQuery));
|
|
@ -5,6 +5,7 @@
|
|||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
{% block custom_head_css_js_create %} {% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="footer fixed">
|
||||
<div class="pull-right">
|
||||
Version <strong>1.3.1-{% include '_build.html' %}</strong> GPLv2.
|
||||
Version <strong>1.3.2-{% include '_build.html' %}</strong> GPLv2.
|
||||
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -16,13 +16,14 @@ class UserLoginForm(AuthenticationForm):
|
|||
max_length=128, strip=False
|
||||
)
|
||||
|
||||
def confirm_login_allowed(self, user):
|
||||
if not user.is_staff:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',)
|
||||
|
||||
class UserLoginCaptchaForm(AuthenticationForm):
|
||||
username = forms.CharField(label=_('Username'), max_length=100)
|
||||
password = forms.CharField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=128, strip=False
|
||||
)
|
||||
|
||||
class UserLoginCaptchaForm(UserLoginForm):
|
||||
captcha = CaptchaField()
|
||||
|
||||
|
||||
|
@ -72,7 +73,7 @@ class UserCreateUpdateForm(forms.ModelForm):
|
|||
'data-placeholder': _('Join user groups')
|
||||
}
|
||||
),
|
||||
'otp_level': forms.RadioSelect()
|
||||
'otp_level': forms.RadioSelect(),
|
||||
}
|
||||
|
||||
def clean_public_key(self):
|
||||
|
|
|
@ -14,6 +14,7 @@ from django.utils import timezone
|
|||
from django.shortcuts import reverse
|
||||
|
||||
from common.utils import get_signer, date_expired_default
|
||||
from common.models import Setting
|
||||
|
||||
|
||||
__all__ = ['User']
|
||||
|
@ -35,6 +36,12 @@ class User(AbstractUser):
|
|||
(1, _('Enable')),
|
||||
(2, _("Force enable")),
|
||||
)
|
||||
SOURCE_LOCAL = 'local'
|
||||
SOURCE_LDAP = 'ldap'
|
||||
SOURCE_CHOICES = (
|
||||
(SOURCE_LOCAL, 'Local'),
|
||||
(SOURCE_LDAP, 'LDAP/AD'),
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
username = models.CharField(
|
||||
max_length=128, unique=True, verbose_name=_('Username')
|
||||
|
@ -82,6 +89,10 @@ class User(AbstractUser):
|
|||
created_by = models.CharField(
|
||||
max_length=30, default='', verbose_name=_('Created by')
|
||||
)
|
||||
source = models.CharField(
|
||||
max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES,
|
||||
verbose_name=_('Source')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
|
@ -248,14 +259,17 @@ class User(AbstractUser):
|
|||
|
||||
@property
|
||||
def otp_enabled(self):
|
||||
return self.otp_level > 0
|
||||
return self.otp_force_enabled or self.otp_level > 0
|
||||
|
||||
@property
|
||||
def otp_force_enabled(self):
|
||||
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
|
||||
if mfa_setting and mfa_setting.cleaned_value:
|
||||
return True
|
||||
return self.otp_level == 2
|
||||
|
||||
def enable_otp(self):
|
||||
if not self.otp_force_enabled:
|
||||
if not self.otp_level == 2:
|
||||
self.otp_level = 1
|
||||
|
||||
def force_enable_otp(self):
|
||||
|
@ -275,6 +289,7 @@ class User(AbstractUser):
|
|||
'is_superuser': self.is_superuser,
|
||||
'role': self.get_role_display(),
|
||||
'groups': [group.name for group in self.groups.all()],
|
||||
'source': self.get_source_display(),
|
||||
'wechat': self.wechat,
|
||||
'phone': self.phone,
|
||||
'otp_level': self.otp_level,
|
||||
|
|
|
@ -26,7 +26,10 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
|||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super(UserSerializer, self).get_field_names(declared_fields, info)
|
||||
fields.extend(['groups_display', 'get_role_display', 'is_valid'])
|
||||
fields.extend([
|
||||
'groups_display', 'get_role_display',
|
||||
'get_source_display', 'is_valid'
|
||||
])
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django_auth_ldap.backend import populate_user
|
||||
# from django.db.models.signals import post_save
|
||||
|
||||
from common.utils import get_logger
|
||||
|
@ -28,3 +29,11 @@ def on_user_create(sender, user=None, **kwargs):
|
|||
logger.info(" - Sending welcome mail ...".format(user.name))
|
||||
if user.email:
|
||||
send_user_created_mail(user)
|
||||
|
||||
|
||||
@receiver(populate_user)
|
||||
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
||||
if user:
|
||||
user.source = user.SOURCE_LDAP
|
||||
user.save()
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
|
|
|
@ -122,10 +122,10 @@
|
|||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).on('click', ".fl_goto", function(){
|
||||
var $form = $('#fl_form');
|
||||
$('<input />', {'name': 'wizard_goto_step', 'value': $(this).data('goto'), 'type': 'hidden'}).appendTo($form);
|
||||
$form.submit();
|
||||
return false;
|
||||
var $form = $('#fl_form');
|
||||
$('<input />', {'name': 'wizard_goto_step', 'value': $(this).data('goto'), 'type': 'hidden'}).appendTo($form);
|
||||
$form.submit();
|
||||
return false;
|
||||
}).on('click', '#fl_submit', function(){
|
||||
var isFinish = $('#fl_submit').html() === "{% trans 'Finish' %}";
|
||||
var noChecked = !$('#acceptTerms').prop('checked');
|
||||
|
@ -137,9 +137,10 @@
|
|||
return false;
|
||||
}
|
||||
}).on('click', '#btn-reset-pubkey', function () {
|
||||
var the_url = '{% url "users:user-pubkey-generate" %}';
|
||||
window.open(the_url, "_blank")
|
||||
})
|
||||
var the_url = '{% url "users:user-pubkey-generate" %}';
|
||||
window.open(the_url, "_blank");
|
||||
$('#fl_form').submit();
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
|
||||
</head>
|
||||
|
||||
|
@ -49,10 +50,20 @@
|
|||
<p class="red-fonts">{{ errors }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required="">
|
||||
<input type="password" id="id_new_password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required="">
|
||||
{# 密码popover #}
|
||||
<div id="container">
|
||||
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
|
||||
<div class="arrow" style="left: 50%;"></div>
|
||||
<h3 class="popover-title" style="display: none;"></h3>
|
||||
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
|
||||
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
|
||||
<div class="popover-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="password-confirm" placeholder="{% trans 'Password again' %}" required="">
|
||||
<input type="password" id="id_confirm_password" class="form-control" name="password-confirm" placeholder="{% trans 'Password again' %}" required="">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary block full-width m-b">{% trans "Setting" %}</button>
|
||||
|
||||
|
@ -79,4 +90,33 @@
|
|||
</body>
|
||||
|
||||
</html>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码强度校验
|
||||
var el = $('#id_password_rules'),
|
||||
idPassword = $('#id_new_password'),
|
||||
idPopover = $('#popover777'),
|
||||
container = $('#container'),
|
||||
progress = $('#id_progress'),
|
||||
password_check_rules = {{ password_check_rules|safe }},
|
||||
minLength = {{ min_length }},
|
||||
top = 146, left = 170;
|
||||
|
||||
// 初始化popover
|
||||
initPopover(container, progress, idPassword, el, password_check_rules);
|
||||
|
||||
// 监听事件
|
||||
idPassword.on('focus', function () {
|
||||
idPopover.css('top', top);
|
||||
idPopover.css('left', left);
|
||||
idPopover.css('display', 'block');
|
||||
});
|
||||
idPassword.on('blur', function () {
|
||||
idPopover.css('display', 'none');
|
||||
});
|
||||
idPassword.on('keyup', function(){
|
||||
var password = idPassword.val();
|
||||
checkPasswordRules(password, minLength);
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -99,6 +99,10 @@
|
|||
{% endif %}
|
||||
</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Source' %}:</td>
|
||||
<td><b>{{ user_object.get_source_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date expired' %}:</td>
|
||||
<td><b>{{ user_object.date_expired|date:"Y-m-j H:i:s" }}</b></td>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Role' %}</th>
|
||||
<th class="text-center">{% trans 'User group' %}</th>
|
||||
<th class="text-center">{% trans 'Source' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
|
@ -65,14 +66,14 @@ function initTable() {
|
|||
var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData;
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('00000000-0000-0000-0000-000000000000', cellData);
|
||||
|
||||
var del_btn = "";
|
||||
|
@ -90,7 +91,7 @@ function initTable() {
|
|||
ajax_url: '{% url "api-users:user-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "name" }, {data: "username" }, {data: "get_role_display" },
|
||||
{data: "groups_display" }, {data: "is_valid" }, {data: "id" }
|
||||
{data: "groups_display" }, {data: "get_source_display" }, {data: "is_valid" }, {data: "id" }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
<link href="{% static "css/plugins/cropper/cropper.min.css" %}" rel="stylesheet">
|
||||
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
|
||||
<style>
|
||||
.crop {
|
||||
|
@ -50,6 +52,16 @@
|
|||
{% csrf_token %}
|
||||
{% bootstrap_field form.old_password layout="horizontal" %}
|
||||
{% bootstrap_field form.new_password layout="horizontal" %}
|
||||
{# 密码popover #}
|
||||
<div id="container">
|
||||
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
|
||||
<div class="arrow" style="left: 50%;"></div>
|
||||
<h3 class="popover-title" style="display: none;"></h3>
|
||||
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
|
||||
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
|
||||
<div class="popover-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.confirm_password layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
|
@ -71,5 +83,34 @@
|
|||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/cropper/cropper.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
var el = $('#id_password_rules'),
|
||||
idPassword = $('#id_new_password'),
|
||||
idPopover = $('#popover777'),
|
||||
container = $('#container'),
|
||||
progress = $('#id_progress'),
|
||||
password_check_rules = {{ password_check_rules|safe }},
|
||||
minLength = {{ min_length }},
|
||||
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
|
||||
left = 377;
|
||||
|
||||
// 初始化popover
|
||||
initPopover(container, progress, idPassword, el, password_check_rules);
|
||||
|
||||
// 监听事件
|
||||
idPassword.on('focus', function () {
|
||||
idPopover.css('top', top);
|
||||
idPopover.css('left', left);
|
||||
idPopover.css('display', 'block');
|
||||
});
|
||||
idPassword.on('blur', function () {
|
||||
idPopover.css('display', 'none');
|
||||
});
|
||||
idPassword.on('keyup', function(){
|
||||
var password = idPassword.val();
|
||||
checkPasswordRules(password, minLength);
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -91,8 +91,15 @@
|
|||
{% else %}
|
||||
{% trans 'Disable' %}
|
||||
{% endif %}
|
||||
{% if mfa_setting %}
|
||||
( {% trans 'Administrator Settings force MFA login' %} )
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-navy">{% trans 'Source' %}</td>
|
||||
<td>{{ user.get_source_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-navy">{% trans 'Date joined' %}</td>
|
||||
<td>{{ user.date_joined|date:"Y-m-d H:i:s" }}</td>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
<div class="form-group">
|
||||
<label class="control-label col-sm-2 col-lg-2" style="padding-top: 0">{% trans 'Or reset by server' %}</label>
|
||||
<div class=" col-sm-9 col-lg-9 ">
|
||||
<a href="{% url 'users:user-pubkey-generate' %}">{% trans 'Reset' %}</a>
|
||||
<a id="reset_pubkey" href="{% url 'users:user-pubkey-generate' %}">{% trans 'Reset' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-line-dashed"></div>
|
||||
|
@ -89,5 +89,10 @@
|
|||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/cropper/cropper.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
}).on('click', '#reset_pubkey', function () {
|
||||
var message = "{% trans 'The new public key has been set successfully, Please download the corresponding private key.' %}";
|
||||
toastr.success(message)
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,5 +4,50 @@
|
|||
{% block user_template_title %}{% trans "Update user" %}{% endblock %}
|
||||
{% block password %}
|
||||
{% 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" %}
|
||||
{% 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 @@
|
|||
#
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import re
|
||||
import pyotp
|
||||
import base64
|
||||
import logging
|
||||
|
@ -18,8 +19,11 @@ from django.core.cache import cache
|
|||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse, get_object_or_none
|
||||
from common.models import Setting
|
||||
from common.forms import SecuritySettingForm
|
||||
from .models import User, LoginLog
|
||||
|
||||
|
||||
logger = logging.getLogger('jumpserver')
|
||||
|
||||
|
||||
|
@ -271,3 +275,60 @@ def generate_otp_uri(request, issuer="Jumpserver"):
|
|||
def check_otp_code(otp_secret_key, otp_code):
|
||||
totp = pyotp.TOTP(otp_secret_key)
|
||||
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
|
|||
|
||||
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_user_or_tmp_user, set_tmp_user_to_cache, get_password_check_rules, check_password_rules
|
||||
from ..tasks import write_login_log_async
|
||||
from .. import forms
|
||||
|
||||
|
@ -82,10 +83,10 @@ class UserLoginView(FormView):
|
|||
user = get_user_or_tmp_user(self.request)
|
||||
|
||||
if user.otp_enabled and user.otp_secret_key:
|
||||
# 1,2 & T
|
||||
# 1,2,mfa_setting & T
|
||||
return reverse('users:login-otp')
|
||||
elif user.otp_enabled and not user.otp_secret_key:
|
||||
# 1,2 & F
|
||||
# 1,2,mfa_setting & F
|
||||
return reverse('users:user-otp-enable-authentication')
|
||||
elif not user.otp_enabled:
|
||||
# 0 & T,F
|
||||
|
@ -211,6 +212,10 @@ class UserResetPasswordView(TemplateView):
|
|||
token = request.GET.get('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:
|
||||
kwargs.update({'errors': _('Token invalid or expired')})
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
@ -227,6 +232,13 @@ class UserResetPasswordView(TemplateView):
|
|||
if not user:
|
||||
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)
|
||||
return HttpResponseRedirect(reverse('users:reset-password-success'))
|
||||
|
||||
|
|
|
@ -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.mixins import JSONResponseMixin
|
||||
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
|
||||
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 ..tasks import write_login_log_async
|
||||
|
||||
|
@ -96,10 +97,29 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
|
|||
success_message = update_success_msg
|
||||
|
||||
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)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
password = form.cleaned_data.get('password')
|
||||
if not password:
|
||||
return super().form_valid(form)
|
||||
|
||||
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):
|
||||
model = User
|
||||
|
@ -318,8 +338,10 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
|
|||
template_name = 'users/user_profile.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
|
||||
context = {
|
||||
'action': _('Profile'),
|
||||
'mfa_setting': mfa_setting.cleaned_value if mfa_setting else False,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -353,9 +375,12 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
|
|||
return self.request.user
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
check_rules, min_length = get_password_check_rules()
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('Password update'),
|
||||
'password_check_rules': check_rules,
|
||||
'min_length': min_length,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -364,6 +389,17 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
|
|||
auth_logout(self.request)
|
||||
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):
|
||||
template_name = 'users/user_pubkey_update.html'
|
||||
|
|
|
@ -51,11 +51,6 @@ class Config:
|
|||
REDIS_HOST = '127.0.0.1'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_PASSWORD = ''
|
||||
BROKER_URL = 'redis://%(password)s%(host)s:%(port)s/3' % {
|
||||
'password': REDIS_PASSWORD,
|
||||
'host': REDIS_HOST,
|
||||
'port': REDIS_PORT,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
|
Loading…
Reference in New Issue