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:
fit2bot
2021-07-30 15:19:00 +08:00
committed by GitHub
parent b56b897260
commit 67f6b1080e
16 changed files with 168 additions and 129 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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