diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 829c13f75..91acd3d34 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -333,7 +333,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin): 'iconSkin': icon_skin, 'meta': { 'type': 'asset', - 'asset': { + 'data': { 'id': self.id, 'hostname': self.hostname, 'ip': self.ip, diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 2e3b890a5..77614276a 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -608,7 +608,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): 'isParent': True, 'open': self.is_org_root(), 'meta': { - 'node': { + 'data': { "id": self.id, "name": self.name, "value": self.value, diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index e1d6ead47..495ff5d50 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -16,9 +16,7 @@ import json import yaml from importlib import import_module from django.urls import reverse_lazy -from django.templatetags.static import static from urllib.parse import urljoin, urlparse -from django.utils.translation import ugettext_lazy as _ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -244,7 +242,7 @@ class Config(dict): 'TERMINAL_TELNET_REGEX': '', 'TERMINAL_COMMAND_STORAGE': {}, - 'SECURITY_MFA_AUTH': False, + 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启 'SECURITY_COMMAND_EXECUTION': True, 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, 'SECURITY_VIEW_AUTH_NEED_MFA': True, @@ -253,6 +251,7 @@ class Config(dict): 'SECURITY_MAX_IDLE_TIME': 30, 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, 'SECURITY_PASSWORD_MIN_LENGTH': 6, + 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': 6, 'SECURITY_PASSWORD_UPPER_CASE': False, 'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_NUMBER': False, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 226aedc62..cc6875083 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -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 +SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_ADMIN_USER_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 diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index f85dee23a..013330888 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 64901bb1d..ba3f42c3b 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-07-27 15:57+0800\n" +"POT-Creation-Date: 2021-07-28 18:36+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,7 +25,7 @@ msgstr "" #: orgs/models.py:24 perms/models/base.py:49 settings/models.py:29 #: terminal/models/storage.py:23 terminal/models/task.py:16 #: terminal/models/terminal.py:100 users/forms/profile.py:32 -#: users/models/group.py:15 users/models/user.py:551 +#: users/models/group.py:15 users/models/user.py:555 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -61,7 +61,7 @@ msgstr "激活中" #: orgs/models.py:27 perms/models/base.py:57 settings/models.py:34 #: terminal/models/storage.py:26 terminal/models/terminal.py:114 #: tickets/models/ticket.py:73 users/models/group.py:16 -#: users/models/user.py:584 xpack/plugins/change_auth_plan/models.py:77 +#: users/models/user.py:588 xpack/plugins/change_auth_plan/models.py:77 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:98 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -99,7 +99,7 @@ msgstr "动作" #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:176 -#: users/models/user.py:747 users/models/user.py:773 +#: users/models/user.py:751 users/models/user.py:777 #: users/serializers/group.py:19 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -179,7 +179,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:176 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:549 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:553 #: users/templates/users/_select_user_modal.html:14 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 @@ -515,7 +515,7 @@ msgstr "标签管理" #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25 -#: orgs/models.py:437 perms/models/base.py:55 users/models/user.py:592 +#: orgs/models.py:437 perms/models/base.py:55 users/models/user.py:596 #: users/serializers/group.py:33 xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/cloud/models.py:104 xpack/plugins/gathered_user/models.py:30 msgid "Created by" @@ -529,7 +529,7 @@ msgstr "创建者" #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:26 #: orgs/models.py:435 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:774 xpack/plugins/cloud/models.py:107 +#: users/models/user.py:778 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -587,7 +587,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:570 +#: assets/models/cluster.py:22 users/models/user.py:574 msgid "Phone" msgstr "手机" @@ -613,7 +613,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:759 +#: users/models/user.py:763 msgid "System" msgstr "系统" @@ -1151,7 +1151,7 @@ msgstr "用户代理" #: audits/models.py:105 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:64 users/models/user.py:573 +#: users/forms/profile.py:64 users/models/user.py:577 #: users/serializers/profile.py:102 msgid "MFA" msgstr "多因子认证" @@ -1641,7 +1641,8 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:463 users/serializers/profile.py:99 +#: settings/serializers/settings.py:148 users/models/user.py:463 +#: users/serializers/profile.py:99 #: users/templates/users/user_verify_mfa.html:32 msgid "Disable" msgstr "禁用" @@ -2020,7 +2021,7 @@ msgstr "" "div>" #: notifications/backends/__init__.py:11 users/forms/profile.py:101 -#: users/models/user.py:553 +#: users/models/user.py:557 msgid "Email" msgstr "邮件" @@ -2213,7 +2214,7 @@ msgstr "组织审计员" msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:434 users/models/user.py:561 +#: orgs/models.py:434 users/models/user.py:565 #: users/templates/users/_select_user_modal.html:15 msgid "Role" msgstr "角色" @@ -2276,7 +2277,7 @@ msgid "Favorite" msgstr "收藏夹" #: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31 -#: users/models/user.py:557 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:561 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -2289,7 +2290,7 @@ msgstr "用户组" #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:77 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81 -#: users/models/user.py:589 +#: users/models/user.py:593 msgid "Date expired" msgstr "失效日期" @@ -2585,60 +2586,72 @@ msgstr "RDP 地址" msgid "RDP visit address, eg: dev.jumpserver.org:3389" msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389" -#: settings/serializers/settings.py:147 +#: settings/serializers/settings.py:149 +msgid "All users" +msgstr "所有用户" + +#: settings/serializers/settings.py:150 +msgid "Only admin users" +msgstr "仅管理员" + +#: settings/serializers/settings.py:152 msgid "Global MFA auth" msgstr "全局启用 MFA 认证" -#: settings/serializers/settings.py:148 -msgid "All user enable MFA" -msgstr "强制所有用户启用多因子认证" +#: settings/serializers/settings.py:155 +msgid "Admin user MFA auth" +msgstr "所有管理员启用 MFA" -#: settings/serializers/settings.py:151 +#: settings/serializers/settings.py:156 +msgid "Admin user enable MFA" +msgstr "强制管理员启用 MFA" + +#: settings/serializers/settings.py:159 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/settings.py:152 +#: settings/serializers/settings.py:160 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/settings.py:155 +#: settings/serializers/settings.py:163 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/settings.py:156 +#: settings/serializers/settings.py:164 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/settings.py:160 +#: settings/serializers/settings.py:168 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/serializers/settings.py:164 +#: settings/serializers/settings.py:172 msgid "Block logon interval" msgstr "禁止登录时间间隔" -#: settings/serializers/settings.py:165 +#: settings/serializers/settings.py:173 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/serializers/settings.py:169 +#: settings/serializers/settings.py:177 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/settings.py:170 +#: settings/serializers/settings.py:178 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/settings.py:174 +#: settings/serializers/settings.py:182 msgid "User password expiration" msgstr "用户密码过期时间" -#: settings/serializers/settings.py:175 +#: settings/serializers/settings.py:183 msgid "" "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 " @@ -2648,53 +2661,57 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:179 +#: settings/serializers/settings.py:187 msgid "Number of repeated historical passwords" msgstr "不能设置近几次密码" -#: settings/serializers/settings.py:180 +#: settings/serializers/settings.py:188 msgid "" "Tip: When the user resets the password, it cannot be the previous n " "historical passwords of the user" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" -#: settings/serializers/settings.py:184 +#: settings/serializers/settings.py:192 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:187 +#: settings/serializers/settings.py:196 +msgid "Admin user password minimum length" +msgstr "管理员密码最小长度" + +#: settings/serializers/settings.py:199 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:189 +#: settings/serializers/settings.py:201 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:190 +#: settings/serializers/settings.py:202 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:191 +#: settings/serializers/settings.py:203 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:192 +#: settings/serializers/settings.py:204 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:194 +#: settings/serializers/settings.py:206 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:195 +#: settings/serializers/settings.py:207 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/settings.py:203 +#: settings/serializers/settings.py:215 msgid "Enable WeCom Auth" msgstr "启用企业微信认证" -#: settings/serializers/settings.py:210 +#: settings/serializers/settings.py:222 msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" @@ -4069,7 +4086,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:160 users/models/user.py:581 +#: users/forms/profile.py:160 users/models/user.py:585 #: users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" @@ -4086,39 +4103,39 @@ msgstr "系统审计员" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:530 +#: users/models/user.py:534 msgid "Local" msgstr "数据库" -#: users/models/user.py:564 +#: users/models/user.py:568 msgid "Avatar" msgstr "头像" -#: users/models/user.py:567 +#: users/models/user.py:571 msgid "Wechat" msgstr "微信" -#: users/models/user.py:578 +#: users/models/user.py:582 msgid "Private key" msgstr "ssh私钥" -#: users/models/user.py:597 +#: users/models/user.py:601 msgid "Source" msgstr "来源" -#: users/models/user.py:601 +#: users/models/user.py:605 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:604 +#: users/models/user.py:608 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:755 +#: users/models/user.py:759 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:758 +#: users/models/user.py:762 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -4709,24 +4726,24 @@ msgstr "" "
\n" " " -#: users/views/profile/otp.py:107 users/views/profile/otp.py:146 -#: users/views/profile/otp.py:166 +#: users/views/profile/otp.py:107 users/views/profile/otp.py:147 +#: users/views/profile/otp.py:167 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" -#: users/views/profile/otp.py:190 +#: users/views/profile/otp.py:191 msgid "MFA enable success" msgstr "多因子认证启用成功" -#: users/views/profile/otp.py:191 +#: users/views/profile/otp.py:192 msgid "MFA enable success, return login page" msgstr "多因子认证启用成功,返回到登录页面" -#: users/views/profile/otp.py:193 +#: users/views/profile/otp.py:194 msgid "MFA disable success" msgstr "多因子认证禁用成功" -#: users/views/profile/otp.py:194 +#: users/views/profile/otp.py:195 msgid "MFA disable success, return login page" msgstr "多因子认证禁用成功,返回登录页面" @@ -5277,6 +5294,9 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "All user enable MFA" +#~ msgstr "强制所有用户启用 MFA" + #~ msgid "Application category" #~ msgstr "应用类别" diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index 85e1c941f..27e3eff54 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -122,6 +122,7 @@ class PublicSettingApi(generics.RetrieveAPIView): "TICKETS_ENABLED": settings.TICKETS_ENABLED, "PASSWORD_RULE": { 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, + 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH, 'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE, 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE, 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, @@ -160,7 +161,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): def get_object(self): items = self.get_fields().keys() - return {item: getattr(settings, item) for item in items} + obj = {item: getattr(settings, item) for item in items} + return obj def parse_serializer_data(self, serializer): data = [] diff --git a/apps/settings/migrations/0002_auto_20210729_1546.py b/apps/settings/migrations/0002_auto_20210729_1546.py new file mode 100644 index 000000000..986818cd4 --- /dev/null +++ b/apps/settings/migrations/0002_auto_20210729_1546.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1 on 2021-07-29 07:46 + +from django.db import migrations + + +def migrate_security_mfa_auth(apps, schema_editor): + setting_model = apps.get_model("settings", "Setting") + db_alias = schema_editor.connection.alias + + mfa_setting = setting_model.objects.using(db_alias).filter(name='SECURITY_MFA_AUTH').first() + if not mfa_setting: + return + + if mfa_setting.value: + mfa_setting.value = 1 + else: + mfa_setting.value = 0 + mfa_setting.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0001_initial'), + ] + + operations = [ + migrations.RunPython(migrate_security_mfa_auth) + ] diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index f2e11c17f..1039fd557 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -143,9 +143,13 @@ class TerminalSettingSerializer(serializers.Serializer): class SecuritySettingSerializer(serializers.Serializer): - SECURITY_MFA_AUTH = serializers.BooleanField( - required=False, label=_("Global MFA auth"), - help_text=_('All user enable MFA') + SECURITY_MFA_AUTH = serializers.ChoiceField( + choices=( + [0, _('Disable')], + [1, _('All users')], + [2, _('Only admin users')], + ), + required=False, label=_("Global MFA auth") ) SECURITY_COMMAND_EXECUTION = serializers.BooleanField( required=False, label=_('Batch command execution'), @@ -183,6 +187,10 @@ class SecuritySettingSerializer(serializers.Serializer): min_value=6, max_value=30, required=True, label=_('Password minimum length') ) + SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = serializers.IntegerField( + min_value=6, max_value=30, required=True, + label=_('Admin user password minimum length') + ) SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField( required=False, label=_('Must contain capital') ) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index a8a60f44a..f921058fa 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -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 diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index c1caf1f99..3ba22ac7b 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -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' diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index f9b2c46ee..c718f29ec 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -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 diff --git a/apps/users/utils.py b/apps/users/utils.py index 374ead56f..9d4d3e32d 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -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) diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py index caed50532..7966dda8e 100644 --- a/apps/users/views/profile/otp.py +++ b/apps/users/views/profile/otp.py @@ -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 diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py index e2cd8f8e2..e2dde8e92 100644 --- a/apps/users/views/profile/password.py +++ b/apps/users/views/profile/password.py @@ -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 diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index ba9cfd9b7..46d09ab7e 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -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)