mirror of https://github.com/jumpserver/jumpserver
feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 (#6010)
* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 判断是否为历史密码逻辑修改 * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 提示内容更人性化 * fixs: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 最新国际化翻译文件pull/6064/head
parent
4519ccfe1a
commit
11e5a97f14
|
@ -259,6 +259,7 @@ class Config(dict):
|
||||||
'FTP_LOG_KEEP_DAYS': 200,
|
'FTP_LOG_KEEP_DAYS': 200,
|
||||||
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
||||||
'SECURITY_MFA_VERIFY_TTL': 3600,
|
'SECURITY_MFA_VERIFY_TTL': 3600,
|
||||||
|
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
|
||||||
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
|
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
|
||||||
'SYSLOG_ADDR': '', # '192.168.0.1:514'
|
'SYSLOG_ADDR': '', # '192.168.0.1:514'
|
||||||
'SYSLOG_FACILITY': 'user',
|
'SYSLOG_FACILITY': 'user',
|
||||||
|
|
|
@ -38,6 +38,7 @@ SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
|
||||||
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
||||||
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
|
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
|
||||||
|
OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||||
SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE
|
SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE
|
||||||
SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
|
SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
|
||||||
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER
|
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER
|
||||||
|
|
Binary file not shown.
|
@ -2283,35 +2283,46 @@ msgstr ""
|
||||||
"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
|
"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
|
||||||
"提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户"
|
"提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:172
|
#: settings/serializers/settings.py:168
|
||||||
|
msgid "Number of repeated historical passwords"
|
||||||
|
msgstr "历史密码可重复次数"
|
||||||
|
|
||||||
|
#: settings/serializers/settings.py:169
|
||||||
|
msgid ""
|
||||||
|
"Tip: When the user resets the password, it cannot be the previous n "
|
||||||
|
"historical passwords of the user (the value of n here is the value filled in "
|
||||||
|
"the input box)"
|
||||||
|
msgstr "提示:用户重置密码时,不能为该用户前n次历史密码 (此处的n值即为输入框中填写的值)"
|
||||||
|
|
||||||
|
#: settings/serializers/settings.py:173
|
||||||
msgid "Password minimum length"
|
msgid "Password minimum length"
|
||||||
msgstr "密码最小长度"
|
msgstr "密码最小长度"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:175
|
#: settings/serializers/settings.py:176
|
||||||
msgid "Must contain capital"
|
msgid "Must contain capital"
|
||||||
msgstr "必须包含大写字符"
|
msgstr "必须包含大写字符"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:177
|
#: settings/serializers/settings.py:178
|
||||||
msgid "Must contain lowercase"
|
msgid "Must contain lowercase"
|
||||||
msgstr "必须包含小写字符"
|
msgstr "必须包含小写字符"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:178
|
#: settings/serializers/settings.py:179
|
||||||
msgid "Must contain numeric"
|
msgid "Must contain numeric"
|
||||||
msgstr "必须包含数字"
|
msgstr "必须包含数字"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:179
|
#: settings/serializers/settings.py:180
|
||||||
msgid "Must contain special"
|
msgid "Must contain special"
|
||||||
msgstr "必须包含特殊字符"
|
msgstr "必须包含特殊字符"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:180
|
#: settings/serializers/settings.py:181
|
||||||
msgid "Insecure command alert"
|
msgid "Insecure command alert"
|
||||||
msgstr "危险命令告警"
|
msgstr "危险命令告警"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:182
|
#: settings/serializers/settings.py:183
|
||||||
msgid "Email recipient"
|
msgid "Email recipient"
|
||||||
msgstr "邮件收件人"
|
msgstr "邮件收件人"
|
||||||
|
|
||||||
#: settings/serializers/settings.py:183
|
#: settings/serializers/settings.py:184
|
||||||
msgid "Multiple user using , split"
|
msgid "Multiple user using , split"
|
||||||
msgstr "多个用户,使用 , 分割"
|
msgstr "多个用户,使用 , 分割"
|
||||||
|
|
||||||
|
@ -3721,7 +3732,11 @@ msgstr "旧密码错误"
|
||||||
msgid "Password does not match security rules"
|
msgid "Password does not match security rules"
|
||||||
msgstr "密码不满足安全规则"
|
msgstr "密码不满足安全规则"
|
||||||
|
|
||||||
#: users/serializers/profile.py:43
|
#: users/serializers/profile.py:40
|
||||||
|
msgid "The new password cannot be the last {} passwords"
|
||||||
|
msgstr "新密码不能是最近 {} 次的密码"
|
||||||
|
|
||||||
|
#: users/serializers/profile.py:48
|
||||||
msgid "The newly set password is inconsistent"
|
msgid "The newly set password is inconsistent"
|
||||||
msgstr "两次密码不一致"
|
msgstr "两次密码不一致"
|
||||||
|
|
||||||
|
@ -4361,6 +4376,10 @@ msgstr "重置密码成功,返回到登录页面"
|
||||||
msgid "Token invalid or expired"
|
msgid "Token invalid or expired"
|
||||||
msgstr "Token错误或失效"
|
msgstr "Token错误或失效"
|
||||||
|
|
||||||
|
#: users/views/profile/reset.py:133
|
||||||
|
msgid "* The new password cannot be the last {} passwords"
|
||||||
|
msgstr "* 新密码不能是最近 {} 次的密码"
|
||||||
|
|
||||||
#: users/views/profile/reset.py:120
|
#: users/views/profile/reset.py:120
|
||||||
msgid "User auth from {}, go there change password"
|
msgid "User auth from {}, go there change password"
|
||||||
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
|
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
|
||||||
|
|
|
@ -112,6 +112,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
||||||
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
|
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
|
||||||
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||||
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
|
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
|
||||||
|
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
||||||
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
||||||
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
||||||
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||||
|
|
|
@ -167,6 +167,11 @@ class SecuritySettingSerializer(serializers.Serializer):
|
||||||
label=_('User password expiration'),
|
label=_('User password expiration'),
|
||||||
help_text=_('Tip: (unit: day) If the user does not update the password during the time, the user password will expire failure;The password expiration reminder mail will be automatic sent to the user by system within 5 days (daily) before the password expires')
|
help_text=_('Tip: (unit: day) If the user does not update the password during the time, the user password will expire failure;The password expiration reminder mail will be automatic sent to the user by system within 5 days (daily) before the password expires')
|
||||||
)
|
)
|
||||||
|
OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField(
|
||||||
|
min_value=0, max_value=99999, required=True,
|
||||||
|
label=_('Number of repeated historical passwords'),
|
||||||
|
help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user (the value of n here is the value filled in the input box)')
|
||||||
|
)
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
|
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
|
||||||
min_value=6, max_value=30, required=True,
|
min_value=6, max_value=30, required=True,
|
||||||
label=_('Password minimum length')
|
label=_('Password minimum length')
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.1 on 2021-04-27 12:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0031_auto_20201118_1801'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserPasswordHistory',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('password', models.CharField(max_length=128)),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,8 +7,11 @@ import string
|
||||||
import random
|
import random
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
|
@ -70,6 +73,22 @@ class AuthMixin:
|
||||||
def can_use_ssh_key_login():
|
def can_use_ssh_key_login():
|
||||||
return settings.TERMINAL_PUBLIC_KEY_AUTH
|
return settings.TERMINAL_PUBLIC_KEY_AUTH
|
||||||
|
|
||||||
|
def is_history_password(self, password):
|
||||||
|
allow_history_password_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||||
|
history_passwords = self.history_passwords.all().order_by('-date_created')[:int(allow_history_password_count)]
|
||||||
|
|
||||||
|
for history_password in history_passwords:
|
||||||
|
if check_password(password, history_password.password):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_history_password(self, password):
|
||||||
|
UserPasswordHistory.objects.create(
|
||||||
|
user=self, password=make_password(password),
|
||||||
|
date_created=self.date_password_last_updated
|
||||||
|
)
|
||||||
|
|
||||||
def is_public_key_valid(self):
|
def is_public_key_valid(self):
|
||||||
"""
|
"""
|
||||||
Check if the user's ssh public key is valid.
|
Check if the user's ssh public key is valid.
|
||||||
|
@ -729,3 +748,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
||||||
if self.email and self.source == self.Source.local.value:
|
if self.email and self.source == self.Source.local.value:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordHistory(models.Model):
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
|
password = models.CharField(max_length=128)
|
||||||
|
user = models.ForeignKey("users.User", related_name='history_passwords',
|
||||||
|
on_delete=models.CASCADE, verbose_name=_('User'))
|
||||||
|
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||||
|
|
|
@ -30,12 +30,17 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
|
||||||
raise serializers.ValidationError(msg)
|
raise serializers.ValidationError(msg)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
def validate_new_password(self, value):
|
||||||
def validate_new_password(value):
|
|
||||||
from ..utils import check_password_rules
|
from ..utils import check_password_rules
|
||||||
if not check_password_rules(value):
|
if not check_password_rules(value):
|
||||||
msg = _('Password does not match security rules')
|
msg = _('Password does not match security rules')
|
||||||
raise serializers.ValidationError(msg)
|
raise serializers.ValidationError(msg)
|
||||||
|
if self.instance.is_history_password(value):
|
||||||
|
limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||||
|
msg = _('The new password cannot be the last {} passwords').format(limit_count)
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
else:
|
||||||
|
self.instance.save_history_password(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_new_password_again(self, value):
|
def validate_new_password_again(self, value):
|
||||||
|
|
|
@ -128,6 +128,14 @@ class UserResetPasswordView(FormView):
|
||||||
form.add_error('new_password', error)
|
form.add_error('new_password', error)
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
if user.is_history_password(password):
|
||||||
|
limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||||
|
error = _('* The new password cannot be the last {} passwords').format(limit_count)
|
||||||
|
form.add_error('new_password', error)
|
||||||
|
return self.form_invalid(form)
|
||||||
|
else:
|
||||||
|
user.save_history_password(password)
|
||||||
|
|
||||||
user.reset_password(password)
|
user.reset_password(password)
|
||||||
User.expired_reset_password_token(token)
|
User.expired_reset_password_token(token)
|
||||||
send_reset_password_success_mail(self.request, user)
|
send_reset_password_success_mail(self.request, user)
|
||||||
|
|
Loading…
Reference in New Issue