mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-01-29 21:51:31 +00:00
feat: 管理员和普通用户支持单独设置MFA和密码长度 (#6562)
* feat: 支持配置系统管理员强制MFA和独立密码长度限制 * feat: 支持配置系统管理员强制MFA和独立密码长度限制 * feat: 支持配置系统管理员强制MFA和独立密码长度限制, 翻译文件 * fix: 设置界面可设置管理员用户开启MFA,当在设置开启全局的时候,不改变用户的mfa字段状态 * fix: 修改管理员最小密码长度变量名称 * perf: 优化不同的配置 * perf: 修改check password rule * perf: 添加配置文件 * perf: 修改profile * perf: 优化代码 * fix: 修复bug Co-authored-by: fit2cloud-jiangweidong <weidong.jiang@fit2cloud.com> Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
@@ -464,14 +464,19 @@ class MFAMixin:
|
||||
(1, _('Enable')),
|
||||
(2, _("Force enable")),
|
||||
)
|
||||
is_org_admin: bool
|
||||
|
||||
@property
|
||||
def mfa_enabled(self):
|
||||
return self.mfa_force_enabled or self.mfa_level > 0
|
||||
if self.mfa_force_enabled:
|
||||
return True
|
||||
return self.mfa_level > 0
|
||||
|
||||
@property
|
||||
def mfa_force_enabled(self):
|
||||
if settings.SECURITY_MFA_AUTH:
|
||||
if settings.SECURITY_MFA_AUTH in [True, 1]:
|
||||
return True
|
||||
if settings.SECURITY_MFA_AUTH == 2 and self.is_org_admin:
|
||||
return True
|
||||
return self.mfa_level == 2
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
|
||||
|
||||
def validate_new_password(self, value):
|
||||
from ..utils import check_password_rules
|
||||
if not check_password_rules(value):
|
||||
if not check_password_rules(value, user=self.instance):
|
||||
msg = _('Password does not match security rules')
|
||||
raise serializers.ValidationError(msg)
|
||||
if self.instance.is_history_password(value):
|
||||
@@ -106,7 +106,8 @@ class UserProfileSerializer(UserSerializer):
|
||||
fields = UserSerializer.Meta.fields + [
|
||||
'public_key_comment', 'public_key_hash_md5',
|
||||
'admin_or_audit_orgs', 'current_org_roles',
|
||||
'guide_url', 'user_all_orgs'
|
||||
'guide_url', 'user_all_orgs', 'is_org_admin',
|
||||
'is_superuser'
|
||||
]
|
||||
read_only_fields = [
|
||||
'date_joined', 'last_login', 'created_by', 'source'
|
||||
|
||||
@@ -122,7 +122,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
|
||||
if self.instance and not password:
|
||||
# 更新用户, 未设置密码
|
||||
return
|
||||
if not check_password_rules(password):
|
||||
if not check_password_rules(password, user=self.instance):
|
||||
msg = _('Password does not match security rules')
|
||||
raise serializers.ValidationError(msg)
|
||||
return password
|
||||
|
||||
@@ -295,10 +295,12 @@ def check_otp_code(otp_secret_key, otp_code):
|
||||
return totp.verify(otp=otp_code, valid_window=otp_valid_window)
|
||||
|
||||
|
||||
def get_password_check_rules():
|
||||
def get_password_check_rules(user):
|
||||
check_rules = []
|
||||
for rule in settings.SECURITY_PASSWORD_RULES:
|
||||
key = "id_{}".format(rule.lower())
|
||||
if user.is_org_admin and rule == 'SECURITY_PASSWORD_MIN_LENGTH':
|
||||
rule = 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH'
|
||||
value = getattr(settings, rule)
|
||||
if not value:
|
||||
continue
|
||||
@@ -306,7 +308,7 @@ def get_password_check_rules():
|
||||
return check_rules
|
||||
|
||||
|
||||
def check_password_rules(password):
|
||||
def check_password_rules(password, user):
|
||||
pattern = r"^"
|
||||
if settings.SECURITY_PASSWORD_UPPER_CASE:
|
||||
pattern += '(?=.*[A-Z])'
|
||||
@@ -317,7 +319,11 @@ def check_password_rules(password):
|
||||
if settings.SECURITY_PASSWORD_SPECIAL_CHAR:
|
||||
pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])'
|
||||
pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]'
|
||||
pattern += '.{' + str(settings.SECURITY_PASSWORD_MIN_LENGTH-1) + ',}$'
|
||||
if user.is_org_admin:
|
||||
min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH
|
||||
else:
|
||||
min_length = settings.SECURITY_PASSWORD_MIN_LEN
|
||||
pattern += '.{' + str(min_length-1) + ',}$'
|
||||
match_obj = re.match(pattern, password)
|
||||
return bool(match_obj)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import time
|
||||
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
@@ -33,6 +34,16 @@ logger = get_logger(__name__)
|
||||
class UserOtpEnableStartView(UserVerifyPasswordView):
|
||||
template_name = 'users/user_otp_check_password.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
# 开启了 OTP IN RADIUS 就不用绑定了
|
||||
resp = super().form_valid(form)
|
||||
if settings.OTP_IN_RADIUS:
|
||||
user_id = self.request.session.get('user_id')
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
user.enable_mfa()
|
||||
user.save()
|
||||
return resp
|
||||
|
||||
def get_success_url(self):
|
||||
if settings.OTP_IN_RADIUS:
|
||||
success_url = reverse_lazy('authentication:user-otp-settings-success')
|
||||
@@ -85,7 +96,11 @@ class UserOtpEnableBindView(AuthMixin, TemplateView, FormView):
|
||||
session_user = get_object_or_none(User, pk=user_id)
|
||||
|
||||
if session_user:
|
||||
if all((is_auth_password_time_valid(self.request.session), session_user.mfa_enabled, not session_user.otp_secret_key)):
|
||||
if all((
|
||||
is_auth_password_time_valid(self.request.session),
|
||||
session_user.mfa_enabled,
|
||||
not session_user.otp_secret_key
|
||||
)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -4,65 +4,20 @@ import time
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.edit import UpdateView, FormView
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import (
|
||||
IsValidUser,
|
||||
UserCanUpdatePassword
|
||||
)
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from ... import forms
|
||||
from ...models import User
|
||||
from ...utils import (
|
||||
get_user_or_pre_auth_user,
|
||||
check_password_rules, get_password_check_rules,
|
||||
)
|
||||
|
||||
__all__ = ['UserPasswordUpdateView', 'UserVerifyPasswordView']
|
||||
__all__ = ['UserVerifyPasswordView']
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UserPasswordUpdateView(PermissionsMixin, UpdateView):
|
||||
template_name = 'users/user_password_update.html'
|
||||
model = User
|
||||
form_class = forms.UserPasswordForm
|
||||
success_url = reverse_lazy('users:user-profile')
|
||||
permission_classes = [IsValidUser, UserCanUpdatePassword]
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
check_rules = get_password_check_rules()
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('Password update'),
|
||||
'password_check_rules': check_rules,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
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 UserVerifyPasswordView(FormView):
|
||||
template_name = 'users/user_password_verify.html'
|
||||
form_class = forms.UserCheckPasswordForm
|
||||
@@ -74,9 +29,6 @@ class UserVerifyPasswordView(FormView):
|
||||
if not user:
|
||||
form.add_error("password", _("Password invalid"))
|
||||
return self.form_invalid(form)
|
||||
if not user.mfa_is_otp():
|
||||
user.enable_mfa()
|
||||
user.save()
|
||||
self.request.session['user_id'] = str(user.id)
|
||||
self.request.session['auth_password'] = 1
|
||||
self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||
|
||||
@@ -82,8 +82,9 @@ class UserResetPasswordView(FormView):
|
||||
if not user:
|
||||
context['errors'] = _('Token invalid or expired')
|
||||
context['token_invalid'] = True
|
||||
check_rules = get_password_check_rules()
|
||||
context['password_check_rules'] = check_rules
|
||||
else:
|
||||
check_rules = get_password_check_rules(user)
|
||||
context['password_check_rules'] = check_rules
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -100,7 +101,7 @@ class UserResetPasswordView(FormView):
|
||||
return self.form_invalid(form)
|
||||
|
||||
password = form.cleaned_data['new_password']
|
||||
is_ok = check_password_rules(password)
|
||||
is_ok = check_password_rules(password, user)
|
||||
if not is_ok:
|
||||
error = _('* Your password does not meet the requirements')
|
||||
form.add_error('new_password', error)
|
||||
|
||||
Reference in New Issue
Block a user