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>
pull/6569/head
fit2bot 2021-07-30 15:19:00 +08:00 committed by GitHub
parent b56b897260
commit 67f6b1080e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 168 additions and 129 deletions

View File

@ -333,7 +333,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
'iconSkin': icon_skin, 'iconSkin': icon_skin,
'meta': { 'meta': {
'type': 'asset', 'type': 'asset',
'asset': { 'data': {
'id': self.id, 'id': self.id,
'hostname': self.hostname, 'hostname': self.hostname,
'ip': self.ip, 'ip': self.ip,

View File

@ -608,7 +608,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
'isParent': True, 'isParent': True,
'open': self.is_org_root(), 'open': self.is_org_root(),
'meta': { 'meta': {
'node': { 'data': {
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"value": self.value, "value": self.value,

View File

@ -16,9 +16,7 @@ import json
import yaml import yaml
from importlib import import_module from importlib import import_module
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.templatetags.static import static
from urllib.parse import urljoin, urlparse 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__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
@ -244,7 +242,7 @@ class Config(dict):
'TERMINAL_TELNET_REGEX': '', 'TERMINAL_TELNET_REGEX': '',
'TERMINAL_COMMAND_STORAGE': {}, 'TERMINAL_COMMAND_STORAGE': {},
'SECURITY_MFA_AUTH': False, 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_COMMAND_EXECUTION': True, 'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
'SECURITY_VIEW_AUTH_NEED_MFA': True, 'SECURITY_VIEW_AUTH_NEED_MFA': True,
@ -253,6 +251,7 @@ class Config(dict):
'SECURITY_MAX_IDLE_TIME': 30, 'SECURITY_MAX_IDLE_TIME': 30,
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
'SECURITY_PASSWORD_MIN_LENGTH': 6, 'SECURITY_PASSWORD_MIN_LENGTH': 6,
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': 6,
'SECURITY_PASSWORD_UPPER_CASE': False, 'SECURITY_PASSWORD_UPPER_CASE': False,
'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_LOWER_CASE': False,
'SECURITY_PASSWORD_NUMBER': False, 'SECURITY_PASSWORD_NUMBER': False,

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

Binary file not shown.

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -25,7 +25,7 @@ msgstr ""
#: orgs/models.py:24 perms/models/base.py:49 settings/models.py:29 #: 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/storage.py:23 terminal/models/task.py:16
#: terminal/models/terminal.py:100 users/forms/profile.py:32 #: 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/_select_user_modal.html:13
#: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:37
#: users/templates/users/user_asset_permission.html:154 #: 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 #: orgs/models.py:27 perms/models/base.py:57 settings/models.py:34
#: terminal/models/storage.py:26 terminal/models/terminal.py:114 #: terminal/models/storage.py:26 terminal/models/terminal.py:114
#: tickets/models/ticket.py:73 users/models/group.py:16 #: 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/cloud/models.py:35 xpack/plugins/cloud/models.py:98
#: xpack/plugins/gathered_user/models.py:26 #: xpack/plugins/gathered_user/models.py:26
msgid "Comment" msgid "Comment"
@ -99,7 +99,7 @@ msgstr "动作"
#: terminal/backends/command/models.py:18 #: terminal/backends/command/models.py:18
#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38
#: tickets/models/comment.py:17 users/models/user.py:176 #: 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/serializers/group.py:19
#: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:38
#: users/templates/users/user_asset_permission.html:64 #: users/templates/users/user_asset_permission.html:64
@ -179,7 +179,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
#: applications/serializers/attrs/application_type/vmware_client.py:26 #: applications/serializers/attrs/application_type/vmware_client.py:26
#: assets/models/base.py:176 assets/models/gathered_user.py:15 #: assets/models/base.py:176 assets/models/gathered_user.py:15
#: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 #: 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 #: users/templates/users/_select_user_modal.html:14
#: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:47
#: xpack/plugins/change_auth_plan/models.py:278 #: 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/cluster.py:28 assets/models/cmd_filter.py:26
#: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: 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 #: 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 #: 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 #: xpack/plugins/cloud/models.py:104 xpack/plugins/gathered_user/models.py:30
msgid "Created by" msgid "Created by"
@ -529,7 +529,7 @@ msgstr "创建者"
#: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: 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 #: 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 #: 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" msgid "Date created"
msgstr "创建日期" msgstr "创建日期"
@ -587,7 +587,7 @@ msgstr "带宽"
msgid "Contact" msgid "Contact"
msgstr "联系人" msgstr "联系人"
#: assets/models/cluster.py:22 users/models/user.py:570 #: assets/models/cluster.py:22 users/models/user.py:574
msgid "Phone" msgid "Phone"
msgstr "手机" msgstr "手机"
@ -613,7 +613,7 @@ msgid "Default"
msgstr "默认" msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14 #: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:759 #: users/models/user.py:763
msgid "System" msgid "System"
msgstr "系统" msgstr "系统"
@ -1151,7 +1151,7 @@ msgstr "用户代理"
#: audits/models.py:105 #: audits/models.py:105
#: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: authentication/templates/authentication/login_otp.html:6 #: 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 #: users/serializers/profile.py:102
msgid "MFA" msgid "MFA"
msgstr "多因子认证" msgstr "多因子认证"
@ -1641,7 +1641,8 @@ msgid "Show"
msgstr "显示" msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66 #: 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 #: users/templates/users/user_verify_mfa.html:32
msgid "Disable" msgid "Disable"
msgstr "禁用" msgstr "禁用"
@ -2020,7 +2021,7 @@ msgstr ""
"div>" "div>"
#: notifications/backends/__init__.py:11 users/forms/profile.py:101 #: notifications/backends/__init__.py:11 users/forms/profile.py:101
#: users/models/user.py:553 #: users/models/user.py:557
msgid "Email" msgid "Email"
msgstr "邮件" msgstr "邮件"
@ -2213,7 +2214,7 @@ msgstr "组织审计员"
msgid "GLOBAL" msgid "GLOBAL"
msgstr "全局组织" 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 #: users/templates/users/_select_user_modal.html:15
msgid "Role" msgid "Role"
msgstr "角色" msgstr "角色"
@ -2276,7 +2277,7 @@ msgid "Favorite"
msgstr "收藏夹" msgstr "收藏夹"
#: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31 #: 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:39
#: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_asset_permission.html:67
#: users/templates/users/user_database_app_permission.html:38 #: 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_application.py:77
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81
#: users/models/user.py:589 #: users/models/user.py:593
msgid "Date expired" msgid "Date expired"
msgstr "失效日期" msgstr "失效日期"
@ -2585,60 +2586,72 @@ msgstr "RDP 地址"
msgid "RDP visit address, eg: dev.jumpserver.org:3389" msgid "RDP visit address, eg: dev.jumpserver.org:3389"
msgstr "RDP 访问地址, 如: 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" msgid "Global MFA auth"
msgstr "全局启用 MFA 认证" msgstr "全局启用 MFA 认证"
#: settings/serializers/settings.py:148 #: settings/serializers/settings.py:155
msgid "All user enable MFA" msgid "Admin user MFA auth"
msgstr "强制所有用户启用多因子认证" 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" msgid "Batch command execution"
msgstr "批量命令执行" msgstr "批量命令执行"
#: settings/serializers/settings.py:152 #: settings/serializers/settings.py:160
msgid "Allow user run batch command or not using ansible" msgid "Allow user run batch command or not using ansible"
msgstr "是否允许用户使用 ansible 执行批量命令" msgstr "是否允许用户使用 ansible 执行批量命令"
#: settings/serializers/settings.py:155 #: settings/serializers/settings.py:163
msgid "Enable terminal register" msgid "Enable terminal register"
msgstr "终端注册" msgstr "终端注册"
#: settings/serializers/settings.py:156 #: settings/serializers/settings.py:164
msgid "" msgid ""
"Allow terminal register, after all terminal setup, you should disable this " "Allow terminal register, after all terminal setup, you should disable this "
"for security" "for security"
msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
#: settings/serializers/settings.py:160 #: settings/serializers/settings.py:168
msgid "Limit the number of login failures" msgid "Limit the number of login failures"
msgstr "限制登录失败次数" msgstr "限制登录失败次数"
#: settings/serializers/settings.py:164 #: settings/serializers/settings.py:172
msgid "Block logon interval" msgid "Block logon interval"
msgstr "禁止登录时间间隔" msgstr "禁止登录时间间隔"
#: settings/serializers/settings.py:165 #: settings/serializers/settings.py:173
msgid "" msgid ""
"Tip: (unit/minute) if the user has failed to log in for a limited number of " "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." "times, no login is allowed during this time interval."
msgstr "" msgstr ""
"提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
#: settings/serializers/settings.py:169 #: settings/serializers/settings.py:177
msgid "Connection max idle time" msgid "Connection max idle time"
msgstr "连接最大空闲时间" msgstr "连接最大空闲时间"
#: settings/serializers/settings.py:170 #: settings/serializers/settings.py:178
msgid "If idle time more than it, disconnect connection Unit: minute" msgid "If idle time more than it, disconnect connection Unit: minute"
msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
#: settings/serializers/settings.py:174 #: settings/serializers/settings.py:182
msgid "User password expiration" msgid "User password expiration"
msgstr "用户密码过期时间" msgstr "用户密码过期时间"
#: settings/serializers/settings.py:175 #: settings/serializers/settings.py:183
msgid "" msgid ""
"Tip: (unit: day) If the user does not update the password during the time, " "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 " "the user password will expire failure;The password expiration reminder mail "
@ -2648,53 +2661,57 @@ msgstr ""
"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
"提醒邮件将在密码过期前5天内由系统每天自动发送给用户" "提醒邮件将在密码过期前5天内由系统每天自动发送给用户"
#: settings/serializers/settings.py:179 #: settings/serializers/settings.py:187
msgid "Number of repeated historical passwords" msgid "Number of repeated historical passwords"
msgstr "不能设置近几次密码" msgstr "不能设置近几次密码"
#: settings/serializers/settings.py:180 #: settings/serializers/settings.py:188
msgid "" msgid ""
"Tip: When the user resets the password, it cannot be the previous n " "Tip: When the user resets the password, it cannot be the previous n "
"historical passwords of the user" "historical passwords of the user"
msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
#: settings/serializers/settings.py:184 #: settings/serializers/settings.py:192
msgid "Password minimum length" msgid "Password minimum length"
msgstr "密码最小长度" 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" msgid "Must contain capital"
msgstr "必须包含大写字符" msgstr "必须包含大写字符"
#: settings/serializers/settings.py:189 #: settings/serializers/settings.py:201
msgid "Must contain lowercase" msgid "Must contain lowercase"
msgstr "必须包含小写字符" msgstr "必须包含小写字符"
#: settings/serializers/settings.py:190 #: settings/serializers/settings.py:202
msgid "Must contain numeric" msgid "Must contain numeric"
msgstr "必须包含数字" msgstr "必须包含数字"
#: settings/serializers/settings.py:191 #: settings/serializers/settings.py:203
msgid "Must contain special" msgid "Must contain special"
msgstr "必须包含特殊字符" msgstr "必须包含特殊字符"
#: settings/serializers/settings.py:192 #: settings/serializers/settings.py:204
msgid "Insecure command alert" msgid "Insecure command alert"
msgstr "危险命令告警" msgstr "危险命令告警"
#: settings/serializers/settings.py:194 #: settings/serializers/settings.py:206
msgid "Email recipient" msgid "Email recipient"
msgstr "邮件收件人" msgstr "邮件收件人"
#: settings/serializers/settings.py:195 #: settings/serializers/settings.py:207
msgid "Multiple user using , split" msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割" msgstr "多个用户,使用 , 分割"
#: settings/serializers/settings.py:203 #: settings/serializers/settings.py:215
msgid "Enable WeCom Auth" msgid "Enable WeCom Auth"
msgstr "启用企业微信认证" msgstr "启用企业微信认证"
#: settings/serializers/settings.py:210 #: settings/serializers/settings.py:222
msgid "Enable DingTalk Auth" msgid "Enable DingTalk Auth"
msgstr "启用钉钉认证" msgstr "启用钉钉认证"
@ -4069,7 +4086,7 @@ msgstr "不能和原来的密钥相同"
msgid "Not a valid ssh public key" msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法" 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 #: users/templates/users/user_password_update.html:48
msgid "Public key" msgid "Public key"
msgstr "SSH公钥" msgstr "SSH公钥"
@ -4086,39 +4103,39 @@ msgstr "系统审计员"
msgid "Force enable" msgid "Force enable"
msgstr "强制启用" msgstr "强制启用"
#: users/models/user.py:530 #: users/models/user.py:534
msgid "Local" msgid "Local"
msgstr "数据库" msgstr "数据库"
#: users/models/user.py:564 #: users/models/user.py:568
msgid "Avatar" msgid "Avatar"
msgstr "头像" msgstr "头像"
#: users/models/user.py:567 #: users/models/user.py:571
msgid "Wechat" msgid "Wechat"
msgstr "微信" msgstr "微信"
#: users/models/user.py:578 #: users/models/user.py:582
msgid "Private key" msgid "Private key"
msgstr "ssh私钥" msgstr "ssh私钥"
#: users/models/user.py:597 #: users/models/user.py:601
msgid "Source" msgid "Source"
msgstr "来源" msgstr "来源"
#: users/models/user.py:601 #: users/models/user.py:605
msgid "Date password last updated" msgid "Date password last updated"
msgstr "最后更新密码日期" msgstr "最后更新密码日期"
#: users/models/user.py:604 #: users/models/user.py:608
msgid "Need update password" msgid "Need update password"
msgstr "需要更新密码" msgstr "需要更新密码"
#: users/models/user.py:755 #: users/models/user.py:759
msgid "Administrator" msgid "Administrator"
msgstr "管理员" msgstr "管理员"
#: users/models/user.py:758 #: users/models/user.py:762
msgid "Administrator is the super user of system" msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员" msgstr "Administrator是初始的超级管理员"
@ -4709,24 +4726,24 @@ msgstr ""
" <br>\n" " <br>\n"
" " " "
#: users/views/profile/otp.py:107 users/views/profile/otp.py:146 #: users/views/profile/otp.py:107 users/views/profile/otp.py:147
#: users/views/profile/otp.py:166 #: users/views/profile/otp.py:167
msgid "MFA code invalid, or ntp sync server time" msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确或者服务器端时间不对" msgstr "MFA验证码不正确或者服务器端时间不对"
#: users/views/profile/otp.py:190 #: users/views/profile/otp.py:191
msgid "MFA enable success" msgid "MFA enable success"
msgstr "多因子认证启用成功" msgstr "多因子认证启用成功"
#: users/views/profile/otp.py:191 #: users/views/profile/otp.py:192
msgid "MFA enable success, return login page" msgid "MFA enable success, return login page"
msgstr "多因子认证启用成功,返回到登录页面" msgstr "多因子认证启用成功,返回到登录页面"
#: users/views/profile/otp.py:193 #: users/views/profile/otp.py:194
msgid "MFA disable success" msgid "MFA disable success"
msgstr "多因子认证禁用成功" msgstr "多因子认证禁用成功"
#: users/views/profile/otp.py:194 #: users/views/profile/otp.py:195
msgid "MFA disable success, return login page" msgid "MFA disable success, return login page"
msgstr "多因子认证禁用成功,返回登录页面" msgstr "多因子认证禁用成功,返回登录页面"
@ -5277,6 +5294,9 @@ msgstr "旗舰版"
msgid "Community edition" msgid "Community edition"
msgstr "社区版" msgstr "社区版"
#~ msgid "All user enable MFA"
#~ msgstr "强制所有用户启用 MFA"
#~ msgid "Application category" #~ msgid "Application category"
#~ msgstr "应用类别" #~ msgstr "应用类别"

View File

@ -122,6 +122,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
"TICKETS_ENABLED": settings.TICKETS_ENABLED, "TICKETS_ENABLED": settings.TICKETS_ENABLED,
"PASSWORD_RULE": { "PASSWORD_RULE": {
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, '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_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE, 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
@ -160,7 +161,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
def get_object(self): def get_object(self):
items = self.get_fields().keys() 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): def parse_serializer_data(self, serializer):
data = [] data = []

View File

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

View File

@ -143,9 +143,13 @@ class TerminalSettingSerializer(serializers.Serializer):
class SecuritySettingSerializer(serializers.Serializer): class SecuritySettingSerializer(serializers.Serializer):
SECURITY_MFA_AUTH = serializers.BooleanField( SECURITY_MFA_AUTH = serializers.ChoiceField(
required=False, label=_("Global MFA auth"), choices=(
help_text=_('All user enable MFA') [0, _('Disable')],
[1, _('All users')],
[2, _('Only admin users')],
),
required=False, label=_("Global MFA auth")
) )
SECURITY_COMMAND_EXECUTION = serializers.BooleanField( SECURITY_COMMAND_EXECUTION = serializers.BooleanField(
required=False, label=_('Batch command execution'), required=False, label=_('Batch command execution'),
@ -183,6 +187,10 @@ class SecuritySettingSerializer(serializers.Serializer):
min_value=6, max_value=30, required=True, min_value=6, max_value=30, required=True,
label=_('Password minimum length') 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( SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
required=False, label=_('Must contain capital') required=False, label=_('Must contain capital')
) )

View File

@ -464,14 +464,19 @@ class MFAMixin:
(1, _('Enable')), (1, _('Enable')),
(2, _("Force enable")), (2, _("Force enable")),
) )
is_org_admin: bool
@property @property
def mfa_enabled(self): 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 @property
def mfa_force_enabled(self): 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 True
return self.mfa_level == 2 return self.mfa_level == 2

View File

@ -32,7 +32,7 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
def validate_new_password(self, value): def validate_new_password(self, 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, user=self.instance):
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): if self.instance.is_history_password(value):
@ -106,7 +106,8 @@ class UserProfileSerializer(UserSerializer):
fields = UserSerializer.Meta.fields + [ fields = UserSerializer.Meta.fields + [
'public_key_comment', 'public_key_hash_md5', 'public_key_comment', 'public_key_hash_md5',
'admin_or_audit_orgs', 'current_org_roles', '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 = [ read_only_fields = [
'date_joined', 'last_login', 'created_by', 'source' 'date_joined', 'last_login', 'created_by', 'source'

View File

@ -122,7 +122,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
if self.instance and not password: if self.instance and not password:
# 更新用户, 未设置密码 # 更新用户, 未设置密码
return return
if not check_password_rules(password): if not check_password_rules(password, user=self.instance):
msg = _('Password does not match security rules') msg = _('Password does not match security rules')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
return password 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) return totp.verify(otp=otp_code, valid_window=otp_valid_window)
def get_password_check_rules(): def get_password_check_rules(user):
check_rules = [] check_rules = []
for rule in settings.SECURITY_PASSWORD_RULES: for rule in settings.SECURITY_PASSWORD_RULES:
key = "id_{}".format(rule.lower()) 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) value = getattr(settings, rule)
if not value: if not value:
continue continue
@ -306,7 +308,7 @@ def get_password_check_rules():
return check_rules return check_rules
def check_password_rules(password): def check_password_rules(password, user):
pattern = r"^" pattern = r"^"
if settings.SECURITY_PASSWORD_UPPER_CASE: if settings.SECURITY_PASSWORD_UPPER_CASE:
pattern += '(?=.*[A-Z])' pattern += '(?=.*[A-Z])'
@ -317,7 +319,11 @@ def check_password_rules(password):
if settings.SECURITY_PASSWORD_SPECIAL_CHAR: if settings.SECURITY_PASSWORD_SPECIAL_CHAR:
pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])' pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])'
pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]' 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) match_obj = re.match(pattern, password)
return bool(match_obj) return bool(match_obj)

View File

@ -2,6 +2,7 @@
import time import time
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
@ -33,6 +34,16 @@ logger = get_logger(__name__)
class UserOtpEnableStartView(UserVerifyPasswordView): class UserOtpEnableStartView(UserVerifyPasswordView):
template_name = 'users/user_otp_check_password.html' 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): def get_success_url(self):
if settings.OTP_IN_RADIUS: if settings.OTP_IN_RADIUS:
success_url = reverse_lazy('authentication:user-otp-settings-success') 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) session_user = get_object_or_none(User, pk=user_id)
if session_user: 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 True
return False return False

View File

@ -4,65 +4,20 @@ import time
from django.conf import settings from django.conf import settings
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic.edit import UpdateView, FormView from django.views.generic.edit import FormView
from django.contrib.auth import logout as auth_logout
from common.utils import get_logger from common.utils import get_logger
from common.permissions import (
IsValidUser,
UserCanUpdatePassword
)
from common.mixins.views import PermissionsMixin
from ... import forms from ... import forms
from ...models import User
from ...utils import ( from ...utils import (
get_user_or_pre_auth_user, get_user_or_pre_auth_user,
check_password_rules, get_password_check_rules,
) )
__all__ = ['UserPasswordUpdateView', 'UserVerifyPasswordView'] __all__ = ['UserVerifyPasswordView']
logger = get_logger(__name__) 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): class UserVerifyPasswordView(FormView):
template_name = 'users/user_password_verify.html' template_name = 'users/user_password_verify.html'
form_class = forms.UserCheckPasswordForm form_class = forms.UserCheckPasswordForm
@ -74,9 +29,6 @@ class UserVerifyPasswordView(FormView):
if not user: if not user:
form.add_error("password", _("Password invalid")) form.add_error("password", _("Password invalid"))
return self.form_invalid(form) 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['user_id'] = str(user.id)
self.request.session['auth_password'] = 1 self.request.session['auth_password'] = 1
self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS

View File

@ -82,7 +82,8 @@ class UserResetPasswordView(FormView):
if not user: if not user:
context['errors'] = _('Token invalid or expired') context['errors'] = _('Token invalid or expired')
context['token_invalid'] = True context['token_invalid'] = True
check_rules = get_password_check_rules() else:
check_rules = get_password_check_rules(user)
context['password_check_rules'] = check_rules context['password_check_rules'] = check_rules
return context return context
@ -100,7 +101,7 @@ class UserResetPasswordView(FormView):
return self.form_invalid(form) return self.form_invalid(form)
password = form.cleaned_data['new_password'] password = form.cleaned_data['new_password']
is_ok = check_password_rules(password) is_ok = check_password_rules(password, user)
if not is_ok: if not is_ok:
error = _('* Your password does not meet the requirements') error = _('* Your password does not meet the requirements')
form.add_error('new_password', error) form.add_error('new_password', error)