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

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

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

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

* fixs: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 最新国际化翻译文件
pull/6064/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, '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',

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_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.

View File

@ -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 "用户认证源来自 {}, 请去相应系统修改密码"

View File

@ -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(),

View File

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

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 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"))

View File

@ -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):

View File

@ -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)