feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 (#6010)

* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数

* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 判断是否为历史密码逻辑修改

* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 提示内容更人性化

* fixs: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 最新国际化翻译文件
pull/6063/head
fit2cloud-jiangweidong 2021-04-28 17:03:20 +08:00 committed by GitHub
parent 4519ccfe1a
commit 11e5a97f14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 103 additions and 11 deletions

View File

@ -259,6 +259,7 @@ class Config(dict):
'FTP_LOG_KEEP_DAYS': 200,
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
'SECURITY_MFA_VERIFY_TTL': 3600,
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
'SYSLOG_ADDR': '', # '192.168.0.1:514'
'SYSLOG_FACILITY': 'user',

View File

@ -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_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
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_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER

Binary file not shown.

View File

@ -2283,35 +2283,46 @@ msgstr ""
"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
"提醒邮件将在密码过期前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"
msgstr "密码最小长度"
#: settings/serializers/settings.py:175
#: settings/serializers/settings.py:176
msgid "Must contain capital"
msgstr "必须包含大写字符"
#: settings/serializers/settings.py:177
#: settings/serializers/settings.py:178
msgid "Must contain lowercase"
msgstr "必须包含小写字符"
#: settings/serializers/settings.py:178
#: settings/serializers/settings.py:179
msgid "Must contain numeric"
msgstr "必须包含数字"
#: settings/serializers/settings.py:179
#: settings/serializers/settings.py:180
msgid "Must contain special"
msgstr "必须包含特殊字符"
#: settings/serializers/settings.py:180
#: settings/serializers/settings.py:181
msgid "Insecure command alert"
msgstr "危险命令告警"
#: settings/serializers/settings.py:182
#: settings/serializers/settings.py:183
msgid "Email recipient"
msgstr "邮件收件人"
#: settings/serializers/settings.py:183
#: settings/serializers/settings.py:184
msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割"
@ -3721,7 +3732,11 @@ msgstr "旧密码错误"
msgid "Password does not match security rules"
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"
msgstr "两次密码不一致"
@ -4361,6 +4376,10 @@ msgstr "重置密码成功,返回到登录页面"
msgid "Token invalid or expired"
msgstr "Token错误或失效"
#: users/views/profile/reset.py:133
msgid "* The new password cannot be the last {} passwords"
msgstr "* 新密码不能是最近 {} 次的密码"
#: users/views/profile/reset.py:120
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"

View File

@ -112,6 +112,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
"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_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),

View File

@ -167,6 +167,11 @@ class SecuritySettingSerializer(serializers.Serializer):
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')
)
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(
min_value=6, max_value=30, required=True,
label=_('Password minimum length')

View File

@ -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')),
],
),
]

View File

@ -7,8 +7,11 @@ import string
import random
import datetime
from functools import partial
from django.conf import settings
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.db import models
from django.db.models import TextChoices
@ -70,6 +73,22 @@ class AuthMixin:
def can_use_ssh_key_login():
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):
"""
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:
return True
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"))

View File

@ -30,12 +30,17 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(msg)
return value
@staticmethod
def validate_new_password(value):
def validate_new_password(self, value):
from ..utils import check_password_rules
if not check_password_rules(value):
msg = _('Password does not match security rules')
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
def validate_new_password_again(self, value):

View File

@ -128,6 +128,14 @@ class UserResetPasswordView(FormView):
form.add_error('new_password', error)
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.expired_reset_password_token(token)
send_reset_password_success_mail(self.request, user)