mirror of https://github.com/jumpserver/jumpserver
feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 (#6010)
* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 判断是否为历史密码逻辑修改 * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 提示内容更人性化 * fixs: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 最新国际化翻译文件pull/6063/head
parent
4519ccfe1a
commit
11e5a97f14
|
@ -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',
|
||||
|
|
|
@ -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.
|
@ -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 "用户认证源来自 {}, 请去相应系统修改密码"
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 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"))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue