diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index ad2a3b904..2997b1b61 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -106,7 +106,7 @@ class AssetUser(models.Model): def auto_gen_auth(self): password = str(uuid.uuid4()) private_key, public_key = ssh_key_gen( - username=self.name, password=password + username=self.username, password=password ) self.set_auth(password=password, private_key=private_key, diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo index a343d985a..21cce3fc3 100644 Binary files a/apps/i18n/zh/LC_MESSAGES/django.mo and b/apps/i18n/zh/LC_MESSAGES/django.mo differ diff --git a/apps/i18n/zh/LC_MESSAGES/django.po b/apps/i18n/zh/LC_MESSAGES/django.po index 5c559f24e..5d415863a 100644 --- a/apps/i18n/zh/LC_MESSAGES/django.po +++ b/apps/i18n/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-13 17:27+0800\n" +"POT-Creation-Date: 2018-04-18 20:14+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -154,7 +154,7 @@ msgstr "名称" #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:27 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:13 -#: users/models/authentication.py:45 users/models/user.py:39 +#: users/forms.py:22 users/models/authentication.py:45 users/models/user.py:39 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/login.html:56 #: users/templates/users/login_log_list.html:49 @@ -169,9 +169,11 @@ msgid "Password or private key passphrase" msgstr "密码或密钥密码" #: assets/forms/user.py:25 assets/models/base.py:22 common/forms.py:113 -#: users/forms.py:15 users/forms.py:24 users/templates/users/login.html:59 +#: users/forms.py:15 users/forms.py:24 users/forms.py:36 +#: users/templates/users/login.html:59 #: users/templates/users/reset_password.html:52 #: users/templates/users/user_create.html:11 +#: users/templates/users/user_password_authentication.html:13 #: users/templates/users/user_password_update.html:40 #: users/templates/users/user_profile_update.html:40 #: users/templates/users/user_pubkey_update.html:40 @@ -310,7 +312,7 @@ msgstr "标签管理" #: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:86 perms/models.py:28 perms/models.py:72 #: perms/templates/perms/asset_permission_detail.html:98 -#: users/models/user.py:55 users/templates/users/user_detail.html:99 +#: users/models/user.py:55 users/templates/users/user_detail.html:107 msgid "Created by" msgstr "创建者" @@ -341,10 +343,10 @@ msgstr "创建日期" #: ops/models/adhoc.py:42 perms/models.py:30 perms/models.py:74 #: perms/templates/perms/asset_permission_detail.html:102 terminal/models.py:26 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:52 users/templates/users/user_detail.html:111 +#: users/models/user.py:52 users/templates/users/user_detail.html:119 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_profile.html:114 +#: users/templates/users/user_profile.html:122 msgid "Comment" msgstr "备注" @@ -390,7 +392,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:13 -#: users/models/user.py:285 +#: users/models/user.py:299 msgid "System" msgstr "系统" @@ -428,10 +430,10 @@ msgstr "默认资产组" #: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:219 -#: users/models/user.py:30 users/models/user.py:273 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:231 +#: users/models/user.py:30 users/models/user.py:287 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:13 users/views/user.py:335 +#: users/templates/users/user_group_list.html:13 users/views/user.py:339 msgid "User" msgstr "用户" @@ -536,34 +538,6 @@ msgstr "" msgid "推送系统用户到入资产: {}" msgstr "" -#: assets/templates/assets/_admin_user_setting_modal.html:4 -#: users/templates/users/reset_password.html:57 -#: users/templates/users/user_profile.html:20 -msgid "Setting" -msgstr "设置" - -#: assets/templates/assets/_admin_user_setting_modal.html:9 -#: assets/templates/assets/_asset_import_modal.html:9 -#: users/templates/users/_user_import_modal.html:10 -msgid "Template" -msgstr "模板" - -#: assets/templates/assets/_admin_user_setting_modal.html:10 -#: assets/templates/assets/_asset_import_modal.html:10 -#: users/templates/users/_user_import_modal.html:11 -msgid "Download" -msgstr "下载" - -#: assets/templates/assets/_admin_user_setting_modal.html:13 -#: assets/templates/assets/_asset_import_modal.html:13 -msgid "Asset csv file" -msgstr "资产csv文件" - -#: assets/templates/assets/_admin_user_setting_modal.html:16 -#: assets/templates/assets/_asset_import_modal.html:16 -msgid "If set id, will use this id update asset existed" -msgstr "如果设置了id,则会使用该行信息更新该id的资产" - #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" msgstr "更新用户组" @@ -594,6 +568,24 @@ msgstr "二次验证" msgid "Import asset" msgstr "导入资产" +#: assets/templates/assets/_asset_import_modal.html:9 +#: users/templates/users/_user_import_modal.html:10 +msgid "Template" +msgstr "模板" + +#: assets/templates/assets/_asset_import_modal.html:10 +#: users/templates/users/_user_import_modal.html:11 +msgid "Download" +msgstr "下载" + +#: assets/templates/assets/_asset_import_modal.html:13 +msgid "Asset csv file" +msgstr "资产csv文件" + +#: assets/templates/assets/_asset_import_modal.html:16 +msgid "If set id, will use this id update asset existed" +msgstr "如果设置了id,则会使用该行信息更新该id的资产" + #: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:50 #: templates/_nav.html:23 msgid "Asset list" @@ -637,7 +629,7 @@ msgstr "其它" #: assets/templates/assets/asset_update.html:70 #: assets/templates/assets/domain_create_update.html:16 #: assets/templates/assets/gateway_create_update.html:58 -#: assets/templates/assets/label_create_update.html:16 +#: assets/templates/assets/label_create_update.html:18 #: common/templates/common/basic_setting.html:58 #: common/templates/common/email_setting.html:59 #: common/templates/common/ldap_setting.html:59 @@ -647,7 +639,7 @@ msgstr "其它" #: users/templates/users/_user.html:43 #: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_password_update.html:58 -#: users/templates/users/user_profile.html:151 +#: users/templates/users/user_profile.html:180 #: users/templates/users/user_profile_update.html:63 #: users/templates/users/user_pubkey_update.html:70 #: users/templates/users/user_pubkey_update.html:76 @@ -662,7 +654,7 @@ msgstr "重置" #: assets/templates/assets/asset_update.html:71 #: assets/templates/assets/domain_create_update.html:17 #: assets/templates/assets/gateway_create_update.html:59 -#: assets/templates/assets/label_create_update.html:17 +#: assets/templates/assets/label_create_update.html:19 #: common/templates/common/basic_setting.html:59 #: common/templates/common/email_setting.html:60 #: common/templates/common/ldap_setting.html:60 @@ -740,7 +732,7 @@ msgstr "测试" #: assets/templates/assets/asset_list.html:170 #: assets/templates/assets/domain_detail.html:24 #: assets/templates/assets/domain_detail.html:103 -#: assets/templates/assets/domain_gateway_list.html:90 +#: assets/templates/assets/domain_gateway_list.html:85 #: assets/templates/assets/domain_list.html:42 #: assets/templates/assets/label_list.html:38 #: assets/templates/assets/system_user_detail.html:26 @@ -753,8 +745,8 @@ msgstr "测试" #: users/templates/users/user_group_detail.html:28 #: users/templates/users/user_group_list.html:43 #: users/templates/users/user_list.html:76 -#: users/templates/users/user_profile.html:135 #: users/templates/users/user_profile.html:143 +#: users/templates/users/user_profile.html:172 msgid "Update" msgstr "更新" @@ -764,7 +756,7 @@ msgstr "更新" #: assets/templates/assets/asset_list.html:171 #: assets/templates/assets/domain_detail.html:28 #: assets/templates/assets/domain_detail.html:104 -#: assets/templates/assets/domain_gateway_list.html:91 +#: assets/templates/assets/domain_gateway_list.html:86 #: assets/templates/assets/domain_list.html:43 #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:30 @@ -796,13 +788,13 @@ msgstr "选择节点" #: assets/templates/assets/system_user_detail.html:183 #: assets/templates/assets/system_user_list.html:138 templates/_modal.html:22 #: terminal/templates/terminal/session_detail.html:108 -#: users/templates/users/user_detail.html:339 -#: users/templates/users/user_detail.html:364 -#: users/templates/users/user_detail.html:387 +#: users/templates/users/user_detail.html:357 +#: users/templates/users/user_detail.html:382 +#: users/templates/users/user_detail.html:405 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:86 #: users/templates/users/user_list.html:196 -#: users/templates/users/user_profile.html:185 +#: users/templates/users/user_profile.html:214 msgid "Confirm" msgstr "确认" @@ -852,15 +844,15 @@ msgid "Disk" msgstr "硬盘" #: assets/templates/assets/asset_detail.html:121 -#: users/templates/users/user_detail.html:103 -#: users/templates/users/user_profile.html:88 +#: users/templates/users/user_detail.html:111 +#: users/templates/users/user_profile.html:96 msgid "Date joined" msgstr "创建日期" #: assets/templates/assets/asset_detail.html:137 #: terminal/templates/terminal/session_detail.html:81 -#: users/templates/users/user_detail.html:122 -#: users/templates/users/user_profile.html:126 +#: users/templates/users/user_detail.html:130 +#: users/templates/users/user_profile.html:134 msgid "Quick modify" msgstr "快速修改" @@ -873,7 +865,7 @@ msgstr "快速修改" #: perms/templates/perms/asset_permission_list.html:59 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:128 +#: users/templates/users/user_detail.html:136 #: users/templates/users/user_granted_asset.html:46 #: users/templates/users/user_group_granted_asset.html:46 #: users/templates/users/user_list.html:27 @@ -890,7 +882,8 @@ msgid "Refresh" msgstr "刷新" #: assets/templates/assets/asset_detail.html:300 -#: users/templates/users/user_detail.html:273 +#: users/templates/users/user_detail.html:282 +#: users/templates/users/user_detail.html:304 msgid "Update successfully!" msgstr "更新成功" @@ -978,8 +971,8 @@ msgstr "存在资产,不能删除" #: assets/templates/assets/asset_list.html:595 #: assets/templates/assets/system_user_list.html:133 -#: users/templates/users/user_detail.html:334 -#: users/templates/users/user_detail.html:359 +#: users/templates/users/user_detail.html:352 +#: users/templates/users/user_detail.html:377 #: users/templates/users/user_group_list.html:81 #: users/templates/users/user_list.html:191 msgid "Are you sure?" @@ -1032,7 +1025,7 @@ msgstr "网关列表" msgid "Create gateway" msgstr "创建网关" -#: assets/templates/assets/domain_gateway_list.html:92 +#: assets/templates/assets/domain_gateway_list.html:87 #: common/templates/common/email_setting.html:58 #: common/templates/common/ldap_setting.html:58 msgid "Test connection" @@ -1242,14 +1235,18 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" -#: common/fields.py:26 +#: common/fields.py:30 msgid "Not a valid json" msgstr "不是合法json" -#: common/fields.py:28 +#: common/fields.py:32 msgid "Not a string type" msgstr "不是字符类型" +#: common/fields.py:69 +msgid "Encrypt field using Secret Key" +msgstr "" + #: common/forms.py:70 msgid "Current SITE URL" msgstr "当前站点URL" @@ -1389,7 +1386,7 @@ msgstr "" msgid "discard time" msgstr "" -#: common/models.py:29 +#: common/models.py:29 users/templates/users/user_detail.html:96 msgid "Enabled" msgstr "启用" @@ -1707,8 +1704,8 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: perms/forms.py:18 users/forms.py:176 users/forms.py:181 users/forms.py:193 -#: users/forms.py:223 +#: perms/forms.py:18 users/forms.py:188 users/forms.py:193 users/forms.py:205 +#: users/forms.py:235 msgid "Select users" msgstr "选择用户" @@ -1717,7 +1714,7 @@ msgstr "选择用户" #: perms/templates/perms/asset_permission_list.html:136 templates/_nav.html:14 #: users/models/group.py:25 users/models/user.py:42 #: users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:179 +#: users/templates/users/user_detail.html:188 #: users/templates/users/user_list.html:26 msgid "User group" msgstr "用户组" @@ -1732,8 +1729,8 @@ msgstr "" #: perms/models.py:27 perms/models.py:71 #: perms/templates/perms/asset_permission_detail.html:90 -#: users/models/user.py:54 users/templates/users/user_detail.html:95 -#: users/templates/users/user_profile.html:96 +#: users/models/user.py:54 users/templates/users/user_detail.html:103 +#: users/templates/users/user_profile.html:104 msgid "Date expired" msgstr "失效日期" @@ -1770,7 +1767,7 @@ msgid "Add node to this permission" msgstr "添加节点" #: perms/templates/perms/asset_permission_asset.html:125 -#: users/templates/users/user_detail.html:196 +#: users/templates/users/user_detail.html:205 msgid "Join" msgstr "加入" @@ -1856,13 +1853,13 @@ msgstr "商业支持" msgid "Docs" msgstr "文档" -#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:93 +#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:105 #: users/templates/users/_user.html:36 #: users/templates/users/user_password_update.html:37 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:318 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:322 msgid "Profile" msgstr "个人信息" @@ -1919,13 +1916,13 @@ msgstr "关闭" #: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44 #: users/views/group.py:62 users/views/group.py:79 users/views/group.py:95 -#: users/views/login.py:205 users/views/login.py:254 users/views/user.py:60 -#: users/views/user.py:75 users/views/user.py:95 users/views/user.py:151 -#: users/views/user.py:306 users/views/user.py:353 users/views/user.py:375 +#: users/views/login.py:240 users/views/login.py:289 users/views/user.py:64 +#: users/views/user.py:79 users/views/user.py:99 users/views/user.py:155 +#: users/views/user.py:310 users/views/user.py:357 users/views/user.py:379 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:13 users/views/user.py:61 +#: templates/_nav.html:13 users/views/user.py:65 msgid "User list" msgstr "用户列表" @@ -2231,7 +2228,11 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: users/forms.py:27 users/models/user.py:43 +#: users/forms.py:30 +msgid "Otp_code" +msgstr "" + +#: users/forms.py:39 users/models/user.py:43 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 @@ -2239,57 +2240,57 @@ msgstr "" msgid "Role" msgstr "角色" -#: users/forms.py:29 users/forms.py:139 +#: users/forms.py:41 users/forms.py:151 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:30 users/forms.py:140 +#: users/forms.py:42 users/forms.py:152 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:31 +#: users/forms.py:43 msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:49 users/templates/users/user_detail.html:187 +#: users/forms.py:61 users/templates/users/user_detail.html:196 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:59 users/forms.py:154 +#: users/forms.py:71 users/forms.py:166 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:63 users/forms.py:158 users/serializers.py:42 +#: users/forms.py:75 users/forms.py:170 users/serializers.py:45 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:99 +#: users/forms.py:111 msgid "Old password" msgstr "原来密码" -#: users/forms.py:104 +#: users/forms.py:116 msgid "New password" msgstr "新密码" -#: users/forms.py:109 +#: users/forms.py:121 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:119 +#: users/forms.py:131 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:127 +#: users/forms.py:139 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:141 +#: users/forms.py:153 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:169 users/models/user.py:51 +#: users/forms.py:181 users/models/user.py:51 #: users/templates/users/user_password_update.html:43 -#: users/templates/users/user_profile.html:71 +#: users/templates/users/user_profile.html:79 #: users/templates/users/user_profile_update.html:43 #: users/templates/users/user_pubkey_update.html:43 msgid "Public key" @@ -2319,7 +2320,7 @@ msgstr "Agent" msgid "Date login" msgstr "登录日期" -#: users/models/user.py:29 users/models/user.py:281 +#: users/models/user.py:29 users/models/user.py:295 msgid "Administrator" msgstr "管理员" @@ -2327,15 +2328,18 @@ msgstr "管理员" msgid "Application" msgstr "应用程序" -#: users/models/user.py:34 +#: users/models/user.py:34 users/templates/users/user_profile.html:74 +#: users/templates/users/user_profile.html:155 +#: users/templates/users/user_profile.html:158 msgid "Disable" msgstr "禁用" -#: users/models/user.py:35 +#: users/models/user.py:35 users/templates/users/user_profile.html:72 +#: users/templates/users/user_profile.html:162 msgid "Enable" msgstr "启用" -#: users/models/user.py:36 +#: users/models/user.py:36 users/templates/users/user_profile.html:70 msgid "Force enable" msgstr "强制启用" @@ -2352,11 +2356,11 @@ msgstr "头像" msgid "Wechat" msgstr "微信" -#: users/models/user.py:47 +#: users/models/user.py:47 users/templates/users/user_detail.html:91 msgid "Enable OTP" msgstr "二次验证" -#: users/models/user.py:284 +#: users/models/user.py:298 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -2408,11 +2412,16 @@ msgstr "Step" #: users/templates/users/first_login.html:57 msgid "Previous" -msgstr "" +msgstr "上一步" #: users/templates/users/first_login.html:60 +#: users/templates/users/login_otp.html:66 +#: users/templates/users/user_otp_authentication.html:22 +#: users/templates/users/user_otp_enable_bind.html:25 +#: users/templates/users/user_otp_enable_install_app.html:22 +#: users/templates/users/user_password_authentication.html:21 msgid "Next" -msgstr "" +msgstr "下一步" #: users/templates/users/first_login_done.html:30 msgid "Welcome to use jumpserver, visit " @@ -2447,8 +2456,22 @@ msgstr "Agent" msgid "City" msgstr "城市" +#: users/templates/users/login_otp.html:45 +msgid "二次认证" +msgstr "" + +#: users/templates/users/login_otp.html:64 +#: users/templates/users/user_otp_authentication.html:19 +#: users/templates/users/user_otp_enable_bind.html:18 +msgid "Six figures" +msgstr "6位数字" + +#: users/templates/users/login_otp.html:69 +msgid "Can't provide security? Please contact the administrator" +msgstr "如果不能提供OTP码,请联系管理员" + #: users/templates/users/reset_password.html:45 -#: users/templates/users/user_detail.html:325 users/utils.py:71 +#: users/templates/users/user_detail.html:343 users/utils.py:72 msgid "Reset password" msgstr "重置密码" @@ -2456,8 +2479,13 @@ msgstr "重置密码" msgid "Password again" msgstr "再次输入密码" +#: users/templates/users/reset_password.html:57 +#: users/templates/users/user_profile.html:20 +msgid "Setting" +msgstr "设置" + #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:16 users/views/user.py:75 +#: users/templates/users/user_list.html:16 users/views/user.py:79 msgid "Create user" msgstr "创建用户" @@ -2466,7 +2494,7 @@ msgid "Reset link will be generated and sent to the user. " msgstr "生成重置密码连接,通过邮件发送给用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:152 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:156 msgid "User detail" msgstr "用户详情" @@ -2477,55 +2505,67 @@ msgstr "用户详情" msgid "Asset granted" msgstr "授权的资产" -#: users/templates/users/user_detail.html:107 -#: users/templates/users/user_profile.html:92 +#: users/templates/users/user_detail.html:94 +msgid "Force enabled" +msgstr "强制启用" + +#: users/templates/users/user_detail.html:98 +msgid "Disabled" +msgstr "禁用" + +#: users/templates/users/user_detail.html:115 +#: users/templates/users/user_profile.html:100 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:157 +#: users/templates/users/user_detail.html:151 +msgid "Force enabled OTP" +msgstr "强制启用OTP" + +#: users/templates/users/user_detail.html:166 msgid "Send reset password mail" msgstr "发送重置密码邮件" -#: users/templates/users/user_detail.html:160 -#: users/templates/users/user_detail.html:168 +#: users/templates/users/user_detail.html:169 +#: users/templates/users/user_detail.html:177 msgid "Send" msgstr "发送" -#: users/templates/users/user_detail.html:165 +#: users/templates/users/user_detail.html:174 msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" -#: users/templates/users/user_detail.html:324 +#: users/templates/users/user_detail.html:342 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:335 +#: users/templates/users/user_detail.html:353 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:349 +#: users/templates/users/user_detail.html:367 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:350 +#: users/templates/users/user_detail.html:368 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:360 +#: users/templates/users/user_detail.html:378 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:377 -#: users/templates/users/user_profile.html:174 +#: users/templates/users/user_detail.html:395 +#: users/templates/users/user_profile.html:203 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:378 -#: users/templates/users/user_detail.html:382 -#: users/templates/users/user_profile.html:175 -#: users/templates/users/user_profile.html:180 +#: users/templates/users/user_detail.html:396 +#: users/templates/users/user_detail.html:400 +#: users/templates/users/user_profile.html:204 +#: users/templates/users/user_profile.html:209 msgid "User SSH public key update" msgstr "ssh密钥" @@ -2585,24 +2625,28 @@ msgstr "用户删除失败" msgid "OTP" msgstr "" -#: users/templates/users/user_profile.html:100 users/views/user.py:181 -#: users/views/user.py:235 +#: users/templates/users/user_profile.html:108 users/views/user.py:185 +#: users/views/user.py:239 msgid "User groups" msgstr "用户组" -#: users/templates/users/user_profile.html:132 +#: users/templates/users/user_profile.html:140 msgid "Update password" msgstr "更改密码" -#: users/templates/users/user_profile.html:140 +#: users/templates/users/user_profile.html:148 +msgid "Update otp" +msgstr "更改OTP设置" + +#: users/templates/users/user_profile.html:169 msgid "Update SSH public key" msgstr "更改SSH密钥" -#: users/templates/users/user_profile.html:148 +#: users/templates/users/user_profile.html:177 msgid "Reset public key and download" msgstr "重置并下载SSH密钥" -#: users/templates/users/user_profile.html:178 +#: users/templates/users/user_profile.html:207 msgid "Failed to update SSH public key." msgstr "更新密钥失败" @@ -2622,15 +2666,15 @@ msgstr "更新密钥" msgid "Or reset by server" msgstr "或者重置并下载密钥" -#: users/templates/users/user_update.html:4 users/views/user.py:95 +#: users/templates/users/user_update.html:4 users/views/user.py:99 msgid "Update user" msgstr "更新用户" -#: users/utils.py:35 +#: users/utils.py:36 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:37 +#: users/utils.py:38 #, python-format msgid "" "\n" @@ -2671,7 +2715,7 @@ msgstr "" "
\n" " " -#: users/utils.py:73 +#: users/utils.py:74 #, python-format msgid "" "\n" @@ -2715,11 +2759,11 @@ msgstr "" "
\n" " " -#: users/utils.py:104 +#: users/utils.py:105 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:106 +#: users/utils.py:107 #, python-format msgid "" "\n" @@ -2744,15 +2788,15 @@ msgstr "" "
\n" " " -#: users/utils.py:139 +#: users/utils.py:140 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:141 +#: users/utils.py:142 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:154 +#: users/utils.py:155 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" @@ -2768,78 +2812,102 @@ msgstr "更新用户组" msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:55 +#: users/views/login.py:56 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:97 +#: users/views/login.py:106 users/views/user.py:460 users/views/user.py:485 +msgid "Otp code invalid" +msgstr "otp码认证失败" + +#: users/views/login.py:132 msgid "Logout success" msgstr "退出登录成功" -#: users/views/login.py:98 +#: users/views/login.py:133 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: users/views/login.py:114 +#: users/views/login.py:149 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:127 +#: users/views/login.py:162 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:128 +#: users/views/login.py:163 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:142 +#: users/views/login.py:177 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:143 +#: users/views/login.py:178 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:160 users/views/login.py:173 +#: users/views/login.py:195 users/views/login.py:208 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:169 +#: users/views/login.py:204 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:205 +#: users/views/login.py:240 msgid "First login" msgstr "首次登陆" -#: users/views/login.py:255 +#: users/views/login.py:290 msgid "Login log list" msgstr "登录日志" -#: users/views/user.py:105 +#: users/views/user.py:109 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:210 +#: users/views/user.py:214 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:307 +#: users/views/user.py:311 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:336 +#: users/views/user.py:340 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:354 +#: users/views/user.py:358 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:376 +#: users/views/user.py:380 msgid "Public key update" msgstr "密钥更新" +#: users/views/user.py:419 +msgid "Password invalid" +msgstr "用户名或密码无效" + +#: users/views/user.py:512 +msgid "OTP enable success" +msgstr "OTP 绑定成功" + +#: users/views/user.py:513 +msgid "OTP enable success, return login page" +msgstr "OTP 绑定成功,返回到登录页面" + +#: users/views/user.py:515 +msgid "OTP disable success" +msgstr "OTP 解绑成功" + +#: users/views/user.py:516 +msgid "OTP disable success, return login page" +msgstr "OTP 解绑成功,返回登录页面" + #~ msgid "Add asset" #~ msgstr "添加资产到节点" diff --git a/apps/static/css/otp.css b/apps/static/css/otp.css new file mode 100644 index 000000000..4c9ed2606 --- /dev/null +++ b/apps/static/css/otp.css @@ -0,0 +1,146 @@ +/*公共样式*/ +*{ + margin:0; + padding: 0; + outline: none; +} +a{ + text-decoration: none; + color:black +} +li{ + list-style:none; +} +button{ + outline: none; +} +.red-fonts{ + color: #ed5565; + font-size: 15px; + text-align: center; +} + +/*header样式*/ +header{ + overflow:hidden ; + background: #dedede; + padding:15px 200px; +} +header .logo a{ + float:left; +} +header .logo a:nth-child(2){ + padding-top: 13px; +} +header div:nth-child(1){ + float:left; +} +header div:nth-child(2){ + float:right; + font-size: 12px; + padding-top: 20px; +} +header div:nth-child(2) a:hover{ + color:#1ab394; +} + +/*article样式*/ +article{ + padding-top: 50px; + padding:50px 370px +} +article ul{ + float: left; + position: relative; + left: 50%; + margin-bottom: 50px; +} +article ul li{ + float: left; + position: relative; + right: 50%; +} +article ul li span,article ul li i{ + display: block; + float: left; +} +article ul li span{ + width: 150px; + height: 4px; + margin: 15px 0; + background: black; +} +article ul li:last-child{ + padding-left: 2px; +} +.iconfont{ + font-size: 30px; + color: grey; +} +.back{ + margin-left:-15px; +} +.active{ + color:#1ab394; +} +.clearfix:after { + content:""; + height:0; + visibility:hidden; + display:block; + clear:both; +} +.verify{ + text-align: center; + font-size: 14px; + /*padding-left:70px;*/ + color: grey; +} +.verify span{ + color:red; +} +.line{ + width: 500px; + height:1px; + margin-left:100px; + margin-top:10px ; + background: grey; +} + +/*输入框样式*/ +.form-input{ + text-align: center; + margin: 20px auto; +} +.form-input input{ + width: 200px; + height: 30px; + padding-left: 10px; + outline: none; +} +/*身份验证*/ +/*安装应用*/ +.verify div{ + display: inline-block; +} +.verify div:nth-child(3){ + margin-left: 58px; +} +.next{ + margin: 20px auto; + display: block; + width: 214px; + line-height: 34px; + background: #1ab394; + text-align: center; + border-radius: 6px; + color: white; +} +/*绑定TOTP*/ + +/*版权信息*/ +footer{ + text-align:center; + font-size: 14px; + color: #1a1a1a; +} \ No newline at end of file diff --git a/apps/static/fonts/font_otp/iconfont.css b/apps/static/fonts/font_otp/iconfont.css new file mode 100644 index 000000000..bcc9331ef --- /dev/null +++ b/apps/static/fonts/font_otp/iconfont.css @@ -0,0 +1,25 @@ + +@font-face {font-family: "iconfont"; + src: url('iconfont.eot?t=1523776860888'); /* IE9*/ + src: url('iconfont.eot?t=1523776860888#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAY4AAsAAAAACVwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFZW7kggY21hcAAAAYAAAAB0AAABuM8DAsdnbHlmAAAB9AAAAjgAAALsJ9wRv2hlYWQAAAQsAAAALwAAADYREYC1aGhlYQAABFwAAAAcAAAAJAfeA4dobXR4AAAEeAAAABMAAAAYF+kAAGxvY2EAAASMAAAADgAAAA4C0gHmbWF4cAAABJwAAAAfAAAAIAEVAF1uYW1lAAAEvAAAAUUAAAJtPlT+fXBvc3QAAAYEAAAANAAAAEtj7FVFeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk/sM4gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDgwVDwzYm7438AQw9zA0AAUZgTJAQAoXgyieJzFkc0NgCAMhV/5McYQZBBPHJ3BOTw5ABN3DWwLFyfwka+0LyUQCiAC8MIhBIAeEFS3uGS+x2Z+wCn1KsvJ3rhw7d2yPDMVWUeyzMuZqN204DfRf1d/lSxes9J/bxN5IueBzoL3gc6Dy0D7uQ7gXrxnFIt4nG2Su2/TUBSH77mO7aTNg1zHdt6JbWq3hIbUj7giipNWhRKoAm0BlaWIh5goDCBFSAxZqiBgqNhZUAVTBTuVWlgZO4CyRAj+jd7ipguVcnW330/6vqNzEIvQ0W9ml0kiAU2iGbSAbiAEXAnUKM6BYthlXAJRYUU5EWUMzVB4TS0zdZBVLiGZVVuXOZ6LQRTyYClm1ShjAxzbwzUwpRxAKpNeJRNZwmzBWNLIb9Kr+AOIBS0b86Zp63wjYRaFYCdMSIqQt0GOZYMYB2JR2JClEBsa4+g2G0uLu4UpXIBwykgv3YkUM+TeK/tJbkIOAXS7IGSK0U+NeDru/5dpSSAp/kwkmExHtLMJ6PwdTwrhnP4H+Q/7s3YDiOmicZQ/nhLxEpKryNWRAJx6AcrgVqUCgAeyxGHUpwOWBaXfB4Vl6eAR3RFs4YAhYkqHfWiCnhIJ0/WT/n9Nukl3CDk4Cf3WsH7C/sp8Z+YQQVmfremaEgU+zol5kD1wy8AojhXnHTduwRFgoN+cRcAs3dungQDN04+9Nz97TANg0aEDIuxRdpgdVubhx+TlxuGv0wx7NIPTVMORLLPq2CVwLNOxNZV3qqYkJni/JSZGsB/W7t+2vbbX2rYq7542Z2vv11/MLY1QWTOL1yt1+1nj8YNSS11udlTMt2srp7zOjfYSFcf1/MPRhzrWsY9/VeIImym69aU1c9Fdbl1ZX7lbv/l6hMjzhfnPrVvTtnup7a5dm91Y7YG//n+HyatVeJxjYGRgYADiTatcAuP5bb4ycLMwgMC1n3IxCPp/AwsDcwOQy8HABBIFACp7CkQAeJxjYGRgYG7438AQw8IAAkCSkQEVsAEARwwCb3icY2FgYGB+ycDAwoCKARKfAQEAAAAAAAB2ALQA5gEyAXYAAHicY2BkYGBgYwhkYGUAASYg5gJCBob/YD4DABFIAXMAeJxlj01OwzAQhV/6B6QSqqhgh+QFYgEo/RGrblhUavdddN+mTpsqiSPHrdQDcB6OwAk4AtyAO/BIJ5s2lsffvHljTwDc4Acejt8t95E9XDI7cg0XuBeuU38QbpBfhJto41W4Rf1N2MczpsJtdGF5g9e4YvaEd2EPHXwI13CNT+E69S/hBvlbuIk7/Aq30PHqwj7mXle4jUcv9sdWL5xeqeVBxaHJIpM5v4KZXu+Sha3S6pxrW8QmU4OgX0lTnWlb3VPs10PnIhVZk6oJqzpJjMqt2erQBRvn8lGvF4kehCblWGP+tsYCjnEFhSUOjDFCGGSIyujoO1Vm9K+xQ8Jee1Y9zed0WxTU/3OFAQL0z1xTurLSeTpPgT1fG1J1dCtuy56UNJFezUkSskJe1rZUQuoBNmVXjhF6XNGJPyhnSP8ACVpuyAAAAHicY2BigAAuBuyAjZGJkZmRhZGVkY2RnYGxgi2lNDM9v5SluCS1gBVEGIJJIwYGAIzACOU=') format('woff'), + url('iconfont.ttf?t=1523776860888') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ + url('iconfont.svg?t=1523776860888#iconfont') format('svg'); /* iOS 4.1- */ +} + +.iconfont { + font-family:"iconfont" !important; + font-size:16px; + font-style:normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-duigou:before { content: "\e632"; } + +.icon-step:before { content: "\e60e"; } + +.icon-step1:before { content: "\e60f"; } + +.icon-step2:before { content: "\e610"; } + diff --git a/apps/static/fonts/font_otp/iconfont.eot b/apps/static/fonts/font_otp/iconfont.eot new file mode 100644 index 000000000..41bc9e0c6 Binary files /dev/null and b/apps/static/fonts/font_otp/iconfont.eot differ diff --git a/apps/static/fonts/font_otp/iconfont.js b/apps/static/fonts/font_otp/iconfont.js new file mode 100644 index 000000000..22342ce9e --- /dev/null +++ b/apps/static/fonts/font_otp/iconfont.js @@ -0,0 +1 @@ +(function(window){var svgSprite='';var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window) \ No newline at end of file diff --git a/apps/static/fonts/font_otp/iconfont.svg b/apps/static/fonts/font_otp/iconfont.svg new file mode 100644 index 000000000..66b68a6a3 --- /dev/null +++ b/apps/static/fonts/font_otp/iconfont.svg @@ -0,0 +1,45 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/static/fonts/font_otp/iconfont.ttf b/apps/static/fonts/font_otp/iconfont.ttf new file mode 100644 index 000000000..5be075c12 Binary files /dev/null and b/apps/static/fonts/font_otp/iconfont.ttf differ diff --git a/apps/static/fonts/font_otp/iconfont.woff b/apps/static/fonts/font_otp/iconfont.woff new file mode 100644 index 000000000..57ca66199 Binary files /dev/null and b/apps/static/fonts/font_otp/iconfont.woff differ diff --git a/apps/static/img/authenticator_android.png b/apps/static/img/authenticator_android.png new file mode 100644 index 000000000..cb357525d Binary files /dev/null and b/apps/static/img/authenticator_android.png differ diff --git a/apps/static/img/authenticator_iphone.png b/apps/static/img/authenticator_iphone.png new file mode 100644 index 000000000..fd5b4e8eb Binary files /dev/null and b/apps/static/img/authenticator_iphone.png differ diff --git a/apps/static/img/otp_auth.png b/apps/static/img/otp_auth.png new file mode 100644 index 000000000..63964a098 Binary files /dev/null and b/apps/static/img/otp_auth.png differ diff --git a/apps/static/js/plugins/qrcode/qrcode.min.js b/apps/static/js/plugins/qrcode/qrcode.min.js new file mode 100755 index 000000000..993e88f39 --- /dev/null +++ b/apps/static/js/plugins/qrcode/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/apps/users/api.py b/apps/users/api.py index 5cedd0c5f..1f7e4f792 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -2,6 +2,7 @@ import uuid from django.core.cache import cache +from django.urls import reverse from rest_framework import generics from rest_framework.permissions import AllowAny, IsAuthenticated @@ -16,7 +17,7 @@ from .tasks import write_login_log_async from .models import User, UserGroup from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \ IsSuperUserOrAppUser -from .utils import check_user_valid, generate_token +from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code from common.mixins import IDInFilterMixin from common.utils import get_logger @@ -129,47 +130,110 @@ class UserToken(APIView): class UserProfile(APIView): permission_classes = (IsValidUser,) + serializer_class = UserSerializer def get(self, request): - return Response(request.user.to_json()) + # return Response(request.user.to_json()) + return Response(self.serializer_class(request.user).data) def post(self, request): - return Response(request.user.to_json()) + return Response(self.serializer_class(request.user).data) + + +class UserOtpAuthApi(APIView): + permission_classes = (AllowAny,) + serializer_class = UserSerializer + + def post(self, request): + otp_code = request.data.get('otp_code', '') + seed = request.data.get('seed', '') + + user = cache.get(seed, None) + if not user: + return Response({'msg': '请先进行用户名和密码验证'}, status=401) + + if not check_otp_code(user.otp_secret_key, otp_code): + return Response({'msg': 'otp认证失败'}, status=401) + + token = generate_token(request, user) + self.write_login_log(request, user) + return Response( + { + 'token': token, + 'user': self.serializer_class(user).data + } + ) + + @staticmethod + def write_login_log(request, user): + login_ip = request.data.get('remote_addr', None) + login_type = request.data.get('login_type', '') + user_agent = request.data.get('HTTP_USER_AGENT', '') + + if not login_ip: + login_ip = get_login_ip(request) + + write_login_log_async.delay( + user.username, ip=login_ip, + type=login_type, user_agent=user_agent, + ) class UserAuthApi(APIView): permission_classes = (AllowAny,) + serializer_class = UserSerializer def post(self, request): + user, msg = self.check_user_valid(request) + + if not user: + return Response({'msg': msg}, status=401) + + if not user.otp_enabled: + token = generate_token(request, user) + self.write_login_log(request, user) + return Response( + { + 'token': token, + 'user': self.serializer_class(user).data + } + ) + + seed = uuid.uuid4().hex + cache.set(seed, user, 300) + return Response( + { + 'code': 101, + 'msg': '请携带seed值,进行OTP二次认证', + 'otp_url': reverse('api-users:user-otp-auth'), + 'seed': seed, + 'user': self.serializer_class(user).data + }, status=300) + + @staticmethod + def check_user_valid(request): username = request.data.get('username', '') password = request.data.get('password', '') public_key = request.data.get('public_key', '') - login_type = request.data.get('login_type', '') - login_ip = request.data.get('remote_addr', None) - user_agent = request.data.get('HTTP_USER_AGENT', '') - - if not login_ip: - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') - - if x_forwarded_for and x_forwarded_for[0]: - login_ip = x_forwarded_for[0] - else: - login_ip = request.META.get("REMOTE_ADDR") - user, msg = check_user_valid( username=username, password=password, public_key=public_key ) + return user, msg - if user: - token = generate_token(request, user) - write_login_log_async.delay( - user.username, ip=login_ip, - type=login_type, user_agent=user_agent, - ) - return Response({'token': token, 'user': user.to_json()}) - else: - return Response({'msg': msg}, status=401) + @staticmethod + def write_login_log(request, user): + login_ip = request.data.get('remote_addr', None) + login_type = request.data.get('login_type', '') + user_agent = request.data.get('HTTP_USER_AGENT', '') + + if not login_ip: + login_ip = get_login_ip(request) + + write_login_log_async.delay( + user.username, ip=login_ip, + type=login_type, user_agent=user_agent, + ) class UserConnectionTokenApi(APIView): diff --git a/apps/users/forms.py b/apps/users/forms.py index cbb9beaf5..03b1e21cb 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -18,6 +18,18 @@ class UserLoginForm(AuthenticationForm): captcha = CaptchaField() +class UserCheckPasswordForm(forms.Form): + username = forms.CharField(label=_('Username'), max_length=100) + password = forms.CharField( + label=_('Password'), widget=forms.PasswordInput, + max_length=128, strip=False + ) + + +class UserCheckOtpCodeForm(forms.Form): + otp_code = forms.CharField(label=_('Otp_code'), max_length=6) + + class UserCreateUpdateForm(forms.ModelForm): role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) password = forms.CharField( diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 6c3b467e9..d64c038d8 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -219,15 +219,20 @@ class User(AbstractUser): def otp_enabled(self): return self.otp_level > 0 - def enabled_otp(self): - self.otp_level = 1 + @property + def otp_force_enabled(self): + return self.otp_level == 2 + + def enable_otp(self): + if not self.otp_force_enabled: + self.otp_level = 1 def force_enable_otp(self): self.otp_level = 2 - @property - def otp_force_enabled(self): - return self.otp_level == 2 + def disable_otp(self): + self.otp_level = 0 + self.otp_secret_key = None def to_json(self): return OrderedDict({ @@ -241,6 +246,7 @@ class User(AbstractUser): 'groups': [group.name for group in self.groups.all()], 'wechat': self.wechat, 'phone': self.phone, + 'otp_level': self.otp_level, 'comment': self.comment, 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') if self.date_expired is not None else None }) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 9d14a1d3a..f1347b0d5 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -19,7 +19,10 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: model = User list_serializer_class = BulkListSerializer - exclude = ['first_name', 'last_name', 'password', '_private_key', '_public_key'] + exclude = [ + 'first_name', 'last_name', 'password', '_private_key', + '_public_key', '_otp_secret_key', 'user_permissions' + ] def get_field_names(self, declared_fields, info): fields = super(UserSerializer, self).get_field_names(declared_fields, info) diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html new file mode 100644 index 000000000..cd9c43edc --- /dev/null +++ b/apps/users/templates/users/_base_otp.html @@ -0,0 +1,87 @@ +{% load static %} +{% load i18n %} + + + + + + Jumpserver + + + + + + + + + +
+ +
+ 首页 + + 文档 + + GitHub +
+
+ + +
+
+
    +
  • +
    + + +
    +
    验证身份
    +
  • +
  • +
    + + +
    +
    安装应用
    +
  • +
  • +
    + + +
    +
    绑定TOTP
    +
  • +
  • +
    + +
    +
    完成
    +
  • +
+
+
+
安全令牌验证  账户 {{ user.username }}  请按照以下步骤完成绑定操作
+
+ + {% block content %} + {% endblock %} +
+
+ + +
+ +
+ {% include '_copyright.html' %} +
+ +
+ + + + diff --git a/apps/users/templates/users/login_otp.html b/apps/users/templates/users/login_otp.html new file mode 100644 index 000000000..80f5dc429 --- /dev/null +++ b/apps/users/templates/users/login_otp.html @@ -0,0 +1,87 @@ +{% load static %} +{% load i18n %} + + + + + + + Jumpserver + + {% include '_head_css_js.html' %} + + + + + + + +
+
+
+

欢迎使用Jumpserver开源堡垒机

+

+ 全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。 +

+

+ 使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。 +

+

+ 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。 +

+

+ 改变世界,从一点点开始。 +

+ +
+
+
+
+ + {% trans '二次认证' %} +
+
+ +
+

账号保护已开启,请根据提示完成以下操作

+
+ +
+

请在手机中打开Google Authenticator应用,输入6位动态码

+
+ +
+ + {% csrf_token %} + {% if 'otp_code' in form.errors %} +

{{ form.otp_code.errors.as_text }}

+ {% endif %} +
+ +
+ + + + {% trans "Can't provide otp code? Please contact the administrator" %} + + +
+
+

+

+
+
+
+
+
+
+ {% include '_copyright.html' %} +
+
+
+ + diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 12a9af86e..ae192c82f 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -87,10 +87,18 @@ {% trans 'Role' %}: {{ user_object.get_role_display }} -{# #} -{# {% trans 'Enable OTP' %}:#} -{# {{ user_object.enable_otp|yesno:"Yes,No,Unknown"}}#} -{# #} + + {% trans 'Enable OTP' %}: + + {% if user_object.otp_force_enabled %} + {% trans 'Force enabled' %} + {% elif user_object.otp_enabled%} + {% trans 'Enabled' %} + {% else %} + {% trans 'Disabled' %} + {% endif %} + + {% trans 'Date expired' %}: {{ user_object.date_expired|date:"Y-m-j H:i:s" }} @@ -137,22 +145,23 @@ + + + + {% trans 'Force enabled OTP' %}: + +
+
+ + +
+
+
-{# #} -{# {% trans 'Enable OTP' %}:#} -{# #} -{#
#} -{#
#} -{# #} -{# #} -{#
#} -{#
#} -{#
#} -{# #} {% trans 'Send reset password mail' %}: @@ -277,19 +286,28 @@ $(document).ready(function() { success_message: success }); }) -{#.on('click', '#enable_otp', function() {#} -{# var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";#} -{# var checked = $(this).prop('checked');#} -{# var body = {#} -{# 'enable_otp': checked#} -{# };#} -{# var success = '{% trans "Update successfully!" %}';#} -{# APIUpdateAttr({#} -{# url: the_url,#} -{# body: JSON.stringify(body),#} -{# success_message: success#} -{# });#} -{# });#} +.on('click', '#force_enable_otp', function() { + var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}"; + var checked = $(this).prop('checked'); + var otp_level; + var otp_secret_key; + if(checked){ + otp_level = 2 + }else{ + otp_level = 0; + otp_secret_key = ''; + } + var body = { + 'otp_level': otp_level, + 'otp_secret_key': otp_secret_key + }; + var success = '{% trans "Update successfully!" %}'; + APIUpdateAttr({ + url: the_url, + body: JSON.stringify(body), + success_message: success + }); + }) .on('click', '#btn_join_group', function() { if (Object.keys(jumpserver.nodes_selected).length === 0) { return false; diff --git a/apps/users/templates/users/user_otp_authentication.html b/apps/users/templates/users/user_otp_authentication.html new file mode 100644 index 000000000..6e13111ff --- /dev/null +++ b/apps/users/templates/users/user_otp_authentication.html @@ -0,0 +1,37 @@ +{% extends 'users/_base_otp.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+

账号保护已开启,请根据提示完成以下操作

+ +

请在手机中打开Google Authenticator应用,输入6为动态码

+
+ +
+ {% csrf_token %} + {% if 'otp_code' in form.errors %} +

{{ form.otp_code.errors.as_text }}

+ {% endif %} + +
+ +
+ + +
+ + + +{% endblock %} + + diff --git a/apps/users/templates/users/user_otp_enable_bind.html b/apps/users/templates/users/user_otp_enable_bind.html new file mode 100644 index 000000000..7e3d19cf9 --- /dev/null +++ b/apps/users/templates/users/user_otp_enable_bind.html @@ -0,0 +1,53 @@ +{% extends 'users/_base_otp.html' %} +{% load static %} +{% load i18n %} + +{% block content %} + +
+

使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码

+ +
+ +
+ {% csrf_token %} + +
+ +
+ + + + + {% if 'otp_code' in form.errors %} +

{{ form.otp_code.errors.as_text }}

+ {% endif %} + +
+
+ + + + + + +{% endblock %} + diff --git a/apps/users/templates/users/user_otp_enable_install_app.html b/apps/users/templates/users/user_otp_enable_install_app.html new file mode 100644 index 000000000..a47075d70 --- /dev/null +++ b/apps/users/templates/users/user_otp_enable_install_app.html @@ -0,0 +1,32 @@ +{% extends 'users/_base_otp.html' %} +{% load i18n %} +{% load static %} + +{% block content %} +
+

请在手机端下载并安装 Google Authenticator 应用

+
+ +

Android手机下载

+
+ +
+ +

iPhone手机下载

+
+ +

+

安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)

+
+ + + + + +{% endblock %} + diff --git a/apps/users/templates/users/user_password_authentication.html b/apps/users/templates/users/user_password_authentication.html new file mode 100644 index 000000000..773700241 --- /dev/null +++ b/apps/users/templates/users/user_password_authentication.html @@ -0,0 +1,25 @@ +{% extends 'users/_base_otp.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+ {% csrf_token %} + +
+ +
+ +
+ +
+ + + + {% if 'password' in form.errors %} +

{{ form.password.errors.as_text }}

+ {% endif %} + +
+{% endblock %} + diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html index f29e1427e..6cda70eb9 100644 --- a/apps/users/templates/users/user_profile.html +++ b/apps/users/templates/users/user_profile.html @@ -65,7 +65,15 @@ {% trans 'OTP' %} - {{ user.otp_enabled|yesno:"Yes,No,Unkown" }} + + {% if user.otp_force_enabled %} + {% trans 'Force enable' %} + {% elif user.otp_enabled%} + {% trans 'Enable' %} + {% else %} + {% trans 'Disable' %} + {% endif %} + {% trans 'Public key' %} @@ -136,6 +144,27 @@ + + {% trans 'Update otp' %}: + + + {% trans 'Disable' %} + {% else %} + {% url 'users:user-otp-disable-authentication' %} + ">{% trans 'Disable' %} + {% endif %} + {% else %} + {% url 'users:user-otp-enable-authentication' %} + ">{% trans 'Enable' %} + {% endif %} + + + + {% trans 'Update SSH public key' %}: diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index ce681c146..683638a4e 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -20,6 +20,7 @@ urlpatterns = [ url(r'^v1/connection-token/$', api.UserConnectionTokenApi.as_view(), name='connection-token'), url(r'^v1/profile/$', api.UserProfile.as_view(), name='user-profile'), url(r'^v1/auth/$', api.UserAuthApi.as_view(), name='user-auth'), + url(r'^v1/otp/auth/$', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), url(r'^v1/users/(?P[0-9a-zA-Z\-]{36})/password/$', api.ChangeUserPasswordApi.as_view(), name='change-user-password'), url(r'^v1/users/(?P[0-9a-zA-Z\-]{36})/password/reset/$', diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index b9d6788ee..2aaf4a9ae 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -10,6 +10,7 @@ urlpatterns = [ # Login view url(r'^login$', views.UserLoginView.as_view(), name='login'), url(r'^logout$', views.UserLogoutView.as_view(), name='logout'), + url(r'^login/otp$', views.UserLoginOtpView.as_view(), name='login-otp'), url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'), url(r'^password/forgot/sendmail-success$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'), @@ -21,6 +22,11 @@ urlpatterns = [ url(r'^profile/password/update/$', views.UserPasswordUpdateView.as_view(), name='user-password-update'), url(r'^profile/pubkey/update/$', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'), url(r'^profile/pubkey/generate/$', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), + url(r'^profile/otp/enable/authentication/$', views.UserOtpEnableAuthenticationView.as_view(), name='user-otp-enable-authentication'), + url(r'^profile/otp/enable/install-app/$', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'), + url(r'^profile/otp/enable/bind/$', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'), + url(r'^profile/otp/disable/authentication/$', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'), + url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'), # User view url(r'^user$', views.UserListView.as_view(), name='user-list'), @@ -34,7 +40,6 @@ urlpatterns = [ url(r'^user/(?P[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), url(r'^user/(?P[0-9a-zA-Z\-]{36})/login-history', views.UserDetailView.as_view(), name='user-login-history'), - # User group view url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'), url(r'^user-group/(?P[0-9a-zA-Z\-]{36})$', views.UserGroupDetailView.as_view(), name='user-group-detail'), diff --git a/apps/users/utils.py b/apps/users/utils.py index c8a5b60a8..94368e0c7 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -1,6 +1,8 @@ # ~*~ coding: utf-8 ~*~ # from __future__ import unicode_literals +import os +import pyotp import base64 import logging import uuid @@ -17,7 +19,6 @@ from common.tasks import send_mail_async from common.utils import reverse, get_object_or_none from .models import User, LoginLog - logger = logging.getLogger('jumpserver') @@ -163,7 +164,7 @@ def generate_token(request, user): remote_addr = request.META.get('REMOTE_ADDR', '') if not isinstance(remote_addr, bytes): remote_addr = remote_addr.encode("utf-8") - remote_addr = base64.b16encode(remote_addr) #.replace(b'=', '') + remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '') token = cache.get('%s_%s' % (user.id, remote_addr)) if not token: token = uuid.uuid4().hex @@ -181,6 +182,16 @@ def validate_ip(ip): return False +def get_login_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') + if x_forwarded_for and x_forwarded_for[0]: + login_ip = x_forwarded_for[0] + else: + login_ip = request.META.get('REMOTE_ADDR', '') + + return login_ip + + def write_login_log(username, type='', ip='', user_agent=''): if not (ip and validate_ip(ip)): ip = ip[:15] @@ -211,3 +222,39 @@ def get_ip_city(ip, timeout=10): except ValueError: pass return city + + +def get_tmp_user_from_session(request): + user_id = request.session.get('tmp_user_id') + user = get_object_or_none(User, pk=user_id) + return user + + +def set_tmp_user_to_session(request, user): + request.session['tmp_user_id'] = str(user.id) + + +def redirect_user_first_login_or_index(request, redirect_field_name): + if request.user.is_first_login: + return reverse('users:user-first-login') + return request.POST.get( + redirect_field_name, + request.GET.get(redirect_field_name, reverse('index'))) + + +def generate_otp_uri(request, issuer="Jumpserver"): + if request.user.is_authenticated: + user = request.user + else: + user = get_tmp_user_from_session(request) + otp_secret_key = cache.get(request.session.session_key+'otp_key', '') + if not otp_secret_key: + otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') + cache.set(request.session.session_key+'otp_key', otp_secret_key, 600) + totp = pyotp.TOTP(otp_secret_key) + return totp.provisioning_uri(name=user.username, issuer_name=issuer) + + +def check_otp_code(otp_secret_key, otp_code): + totp = pyotp.TOTP(otp_secret_key) + return totp.verify(otp_code) diff --git a/apps/users/views/login.py b/apps/users/views/login.py index cd8ce8fca..d7a98e174 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -23,13 +23,14 @@ from django.conf import settings from common.utils import get_object_or_none from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from ..models import User, LoginLog -from ..utils import send_reset_password_mail +from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \ + get_tmp_user_from_session, set_tmp_user_to_session from ..tasks import write_login_log_async from .. import forms __all__ = [ - 'UserLoginView', 'UserLogoutView', + 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', 'UserResetPasswordView', 'UserResetPasswordSuccessView', 'UserFirstLoginView', 'LoginLogListView' @@ -53,27 +54,24 @@ class UserLoginView(FormView): def form_valid(self, form): if not self.request.session.test_cookie_worked(): return HttpResponse(_("Please enable cookies and try again.")) - auth_login(self.request, form.get_user()) - x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') - if x_forwarded_for and x_forwarded_for[0]: - login_ip = x_forwarded_for[0] - else: - login_ip = self.request.META.get('REMOTE_ADDR', '') - user_agent = self.request.META.get('HTTP_USER_AGENT', '') - write_login_log_async.delay( - self.request.user.username, type='W', - ip=login_ip, user_agent=user_agent - ) + set_tmp_user_to_session(self.request, form.get_user()) return redirect(self.get_success_url()) def get_success_url(self): - if self.request.user.is_first_login: - return reverse('users:user-first-login') + user = get_tmp_user_from_session(self.request) - return self.request.POST.get( - self.redirect_field_name, - self.request.GET.get(self.redirect_field_name, reverse('index'))) + if user.otp_enabled and user.otp_secret_key: + # 1,2 & T + return reverse('users:login-otp') + elif user.otp_enabled and not user.otp_secret_key: + # 1,2 & F + return reverse('users:user-otp-enable-authentication') + elif not user.otp_enabled: + # 0 & T,F + auth_login(self.request, user) + self.write_login_log() + return redirect_user_first_login_or_index(self.request, self.redirect_field_name) def get_context_data(self, **kwargs): context = { @@ -82,6 +80,44 @@ class UserLoginView(FormView): kwargs.update(context) return super().get_context_data(**kwargs) + def write_login_log(self): + login_ip = get_login_ip(self.request) + user_agent = self.request.META.get('HTTP_USER_AGENT', '') + write_login_log_async.delay( + self.request.user.username, type='W', + ip=login_ip, user_agent=user_agent + ) + + +class UserLoginOtpView(FormView): + template_name = 'users/login_otp.html' + form_class = forms.UserCheckOtpCodeForm + redirect_field_name = 'next' + + def form_valid(self, form): + user = get_tmp_user_from_session(self.request) + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = user.otp_secret_key + + if check_otp_code(otp_secret_key, otp_code): + auth_login(self.request, user) + self.write_login_log() + return redirect(self.get_success_url()) + else: + form.add_error('otp_code', _('Otp code invalid')) + return super().form_invalid(form) + + def get_success_url(self): + return redirect_user_first_login_or_index(self.request, self.redirect_field_name) + + def write_login_log(self): + login_ip = get_login_ip(self.request) + user_agent = self.request.META.get('HTTP_USER_AGENT', '') + write_login_log_async.delay( + self.request.user.username, type='W', + ip=login_ip, user_agent=user_agent + ) + @method_decorator(never_cache, name='dispatch') class UserLogoutView(TemplateView): diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 1d68eb81d..99c45b19c 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -11,6 +11,7 @@ from io import StringIO from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth import authenticate, login as auth_login from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.http import HttpResponse, JsonResponse @@ -34,9 +35,9 @@ from common.mixins import JSONResponseMixin from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from .. import forms from ..models import User, UserGroup -from ..utils import AdminUserRequiredMixin +from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_tmp_user_from_session from ..signals import post_user_create - +from ..tasks import write_login_log_async __all__ = [ 'UserListView', 'UserCreateView', 'UserDetailView', @@ -46,6 +47,9 @@ __all__ = [ 'UserProfileUpdateView', 'UserPasswordUpdateView', 'UserPublicKeyUpdateView', 'UserBulkUpdateView', 'UserPublicKeyGenerateView', + 'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView', + 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView', + 'UserOtpDisableAuthenticationView', ] logger = get_logger(__name__) @@ -380,6 +384,7 @@ class UserPublicKeyUpdateView(LoginRequiredMixin, UpdateView): class UserPublicKeyGenerateView(LoginRequiredMixin, View): + def get(self, request, *args, **kwargs): private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') request.user.public_key = public @@ -389,3 +394,148 @@ class UserPublicKeyGenerateView(LoginRequiredMixin, View): response['Content-Disposition'] = 'attachment; filename={}'.format(filename) return response + +class UserOtpEnableAuthenticationView(FormView): + template_name = 'users/user_password_authentication.html' + form_class = forms.UserCheckPasswordForm + + def get_form(self, form_class=None): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) + form = super().get_form(form_class=form_class) + form['username'].initial = user.username + return form + + def get_context_data(self, **kwargs): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) + context = { + 'user': user + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def form_valid(self, form): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) + password = form.cleaned_data.get('password') + user = authenticate(username=user.username, password=password) + if not user: + form.add_error("password", _("Password invalid")) + return self.form_invalid(form) + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('users:user-otp-enable-install-app') + + +class UserOtpEnableInstallAppView(TemplateView): + template_name = 'users/user_otp_enable_install_app.html' + + def get_context_data(self, **kwargs): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) + context = { + 'user': user + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserOtpEnableBindView(TemplateView, FormView): + template_name = 'users/user_otp_enable_bind.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def get_context_data(self, **kwargs): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) + context = { + 'otp_uri': generate_otp_uri(self.request), + 'user': user + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def form_valid(self, form): + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '') + + if check_otp_code(otp_secret_key, otp_code): + self.save_otp(otp_secret_key) + return super().form_valid(form) + + else: + form.add_error("otp_code", _("Otp code invalid")) + return self.form_invalid(form) + + def save_otp(self, otp_secret_key): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) + user.enable_otp() + user.otp_secret_key = otp_secret_key + user.save() + + +class UserOtpDisableAuthenticationView(FormView): + template_name = 'users/user_otp_authentication.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def form_valid(self, form): + user = self.request.user + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = user.otp_secret_key + + if check_otp_code(otp_secret_key, otp_code): + user.disable_otp() + user.save() + return super().form_valid(form) + else: + form.add_error('otp_code', _('Otp code invalid')) + return super().form_invalid(form) + + +class UserOtpSettingsSuccessView(TemplateView): + template_name = 'flash_message_standalone.html' + + # def get(self, request, *args, **kwargs): + # return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + title, describe = self.get_title_describe() + context = { + 'title': title, + 'messages': describe, + 'interval': 1, + 'redirect_url': reverse('users:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def get_title_describe(self): + if self.request.user.is_authenticated: + user = self.request.user + auth_logout(self.request) + else: + user = get_tmp_user_from_session(self.request) + title = _('OTP enable success') + describe = _('OTP enable success, return login page') + if not user.otp_enabled: + title = _('OTP disable success') + describe = _('OTP disable success, return login page') + + return title, describe diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 790c19eb7..e0ecc634e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -54,6 +54,7 @@ pyasn1==0.4.2 pycparser==2.18 pycrypto==2.6.1 pyldap==2.4.45 +pyotp==2.2.6 PyNaCl==1.2.1 python-dateutil==2.6.1 python-gssapi==0.6.4