Compare commits

...

259 Commits
v4.2 ... v4.6

Author SHA1 Message Date
Bai
482319fadf fix: setting field encrypt issue 2025-02-06 17:15:21 +08:00
Bryan
2ca4002624 Merge pull request #14813 from jumpserver/dev
v4.6.0
2025-01-15 14:38:17 +08:00
wangruidong
543dde57ab perf: modify average_time_cost calculation in job model 2025-01-14 18:28:31 +08:00
w940853815
c088437fe5 Revert "perf: Optimize average_time_cost calculation in job model"
This reverts commit eafb5ecfb3.
2025-01-14 18:18:44 +08:00
feng
e721ec147c perf: luna translate 2025-01-14 17:30:00 +08:00
wangruidong
5d18d6dee0 fix: Add with_expired param to permission utils 2025-01-14 16:41:39 +08:00
feng
ecfd338428 perf: Lina translate 2025-01-14 15:16:17 +08:00
Eric
4b28b079dc perf: fix rdp file resolution value 2025-01-14 13:57:11 +08:00
wangruidong
c1c3236a30 fix: Add redirect_url check in base view 2025-01-14 11:54:17 +08:00
feng
4b19750581 perf: Client version 2025-01-14 10:32:07 +08:00
wangruidong
eafb5ecfb3 perf: Optimize average_time_cost calculation in job model 2025-01-13 17:12:11 +08:00
Bai
583486e26e fix: radius user auth skip backend 2025-01-13 15:49:23 +08:00
Bai
8198620a2e feat: add gitignore 2025-01-09 10:50:57 +08:00
wangruidong
c0b301d52b fix: ldap ha periodic task did not execute as expected 2025-01-08 18:34:53 +08:00
feng
7791d6222a perf: translate 2025-01-08 14:33:04 +08:00
Bai
b740d9d42f fix: circle imported for perms-api 2025-01-08 10:35:13 +08:00
wangruidong
48d0187604 fix: circular import 2025-01-07 14:01:38 +08:00
wangruidong
6217018427 perf: Translate var_name help_text 2025-01-06 11:36:11 +08:00
jiangweidong
923f40e523 feat: VMware automatically syncs folders to node-translation 2025-01-03 18:50:12 +08:00
Bai
1f1fe2084b fix: koko press r dont refresh user perm-nodes 2025-01-03 17:13:32 +08:00
刘瑞斌
b8b1a6ac9c chore: update readme 2024-12-30 16:04:32 +08:00
wangruidong
35f88722af fix: Add type check for secure command execution 2024-12-24 15:58:56 +08:00
Bai
7e6d2749ae fix: core download page error 2024-12-20 16:35:24 +08:00
Bai
be57b101ff fix: set default ldap user dn cache time (0) 2024-12-20 16:35:03 +08:00
Bai
41c8cb6307 fix: api prometheus count 2024-12-20 11:14:55 +08:00
Bai
3a7ae01ede fix: add settings for license version and facelive 2024-12-19 17:37:39 +08:00
Bryan
053d640e4c Merge pull request #14699 from jumpserver/dev
v4.5.0
2024-12-19 16:04:45 +08:00
老广
d17ca4f6a7 Revert "perf: update const import"
This reverts commit 2956f2e4b7.
2024-12-19 16:03:29 +08:00
Bryan
f3acc28ded Merge pull request #14697 from jumpserver/dev
v4.5.0
2024-12-19 15:57:11 +08:00
Aaron3S
5a14bb13d0 feat: remove mfa check when unbind face code 2024-12-19 15:50:38 +08:00
ibuler
2956f2e4b7 perf: update const import 2024-12-19 15:49:13 +08:00
feng
e983ac3cbc perf: Translate 2024-12-19 15:18:39 +08:00
ibuler
fab156dc5f perf: update login success redirect 2024-12-19 14:34:49 +08:00
wangruidong
f6f897317e perf: default_value field allow blank 2024-12-19 12:01:08 +08:00
Aaron3S
a0441cd6ea feat: add translate 2024-12-19 11:13:13 +08:00
Chenyang Shen
e9abd1e72d Merge pull request #14688 from jumpserver/pr@dev@fix_face_openid
fix: fix openid user can't login with face verify
2024-12-19 11:08:45 +08:00
Aaron3S
9fcb4ecba0 fix: fix openid user can't login with face verify 2024-12-19 10:56:44 +08:00
feng
4b637ad86e perf: Client version 3.0.0 2024-12-18 19:33:45 +08:00
jiangweidong
829f867962 perf: The command amount does not record operation logs. 2024-12-18 19:33:02 +08:00
feng
7f965b55f4 perf: Translate 2024-12-18 18:21:43 +08:00
Chenyang Shen
0e0be618e5 Merge pull request #14684 from jumpserver/pr@dev@feat_refresh_cache_facelive
feat: refresh facelive cache
2024-12-18 18:10:39 +08:00
Aaron3S
9577af3221 feat: refresh facelive cache 2024-12-18 18:08:13 +08:00
Aaron3S
a6b7cc9d1b fix: fix 401 error on face verify when use openid login 2024-12-18 18:07:42 +08:00
feng
7a9a71197a perf: Client login 2024-12-18 18:01:38 +08:00
jiangweidong
3cd68ba0a9 perf: push account without increasing version. 2024-12-18 16:51:39 +08:00
jiangweidong
02bdd0f07d perf: push account without increasing version. 2024-12-18 16:51:39 +08:00
jiangweidong
98cf6f82b7 perf: create account add activity log 2024-12-18 15:54:57 +08:00
wangruidong
27fd5d51b9 perf: Translate 2024-12-18 15:53:47 +08:00
wangruidong
095ca91e30 feat: add 'labels' to DomainSerializer fields_m2m 2024-12-18 10:57:30 +08:00
wangruidong
d05514962a fix: calc platform asset count 2024-12-17 19:08:55 +08:00
Bai
c4066a03fa fix: login show system org 2024-12-17 19:08:31 +08:00
Aaron3S
a7d4c4ca2a feat: change face online killer name 2024-12-17 18:53:09 +08:00
Chenyang Shen
5b0f8f63a3 Merge pull request #14670 from jumpserver/pr@dev@feat_add_some_translate
feat: add some translate
2024-12-17 18:24:01 +08:00
Aaron3S
c4bcae68bf feat: add some translate 2024-12-17 18:03:13 +08:00
Aaron3S
29ca50f97e feat: add face online acl check for exchange token 2024-12-17 17:18:37 +08:00
feng
49aaf8d53e perf: Remove the login status after the client logs in 2024-12-17 15:32:39 +08:00
feng
931e15173b perf: perm asset api date_updated order 2024-12-17 14:43:19 +08:00
feng
4018a59b2e perf: Account backup filter org 2024-12-17 11:23:32 +08:00
Chenyang Shen
88905bd28d Merge pull request #14664 from jumpserver/pr@dev@feat_add_face_verify_on_exchange_token
feat: add face verify on exchange connect token
2024-12-16 19:00:56 +08:00
Aaron3S
abad98a190 feat: add face verify on exchange connect token 2024-12-16 18:54:32 +08:00
Chenyang Shen
7419139b29 Merge pull request #14663 from jumpserver/pr@dev@feat_exclude_some_action_for_acl
feat: exclude face action for login acl and command acl
2024-12-16 18:22:09 +08:00
Aaron3S
a1fd3b1ecb feat: exclude face action for login acl and command acl 2024-12-16 18:17:00 +08:00
wangruidong
8a8a7f9947 fix: filter custom assets in secret type check 2024-12-16 17:37:05 +08:00
Chenyang Shen
f9e6fc98fb Merge pull request #14654 from jumpserver/pr@dev@feat_update_migrations
feat: update migrations
2024-12-13 12:42:17 +08:00
Aaron3S
0dd015bcba feat: update migrations 2024-12-13 12:40:06 +08:00
Aaron3S
d1ea31c9a4 feat: face online 2024-12-12 18:31:21 +08:00
feng
e2bf56e624 perf: translate 2024-12-12 15:49:37 +08:00
feng
26040a5560 perf: pt_br translate 2024-12-12 14:40:59 +08:00
Bai
54726f0a2d perf: Passkey Model field token max_length 1024 2024-12-12 14:29:23 +08:00
Eric
7fd88b95f9 perf: update lion i18n 2024-12-12 11:21:19 +08:00
feng
4f271d6405 perf: RBAC remove assets gpt custom 2024-12-11 19:05:33 +08:00
feng
fe17a8c3a0 perf: The entire organization can view activity log 2024-12-11 18:45:41 +08:00
fit2bot
ee5e97e860 perf: add rdp connection speed option (#14641)
* perf: add rdp connection speed option

* perf: remove print code

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 18:42:05 +08:00
fit2bot
dddfc66efd perf: add encrypted configuration API (#14632)
* perf: 添加加密配置API

* perf: modify url

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 11:34:09 +08:00
Bai
d005bd804f fix: user orgs add field: is_system 2024-12-10 19:19:06 +08:00
Bai
08de04fdbc fix: fixed an issue when third-part user auth 2024-12-10 16:41:38 +08:00
Bai
9ed7c41514 fix: fixed an issue when third-part user auth 2024-12-10 16:41:38 +08:00
Eric
1a81b76a46 perf: add new Qwerty for keyboard layout 2024-12-10 15:26:18 +08:00
github-actions[bot]
cf99a7a031 perf: Update Dockerfile with new base image tag 2024-12-10 15:25:37 +08:00
Bai
64551b13a1 feat: deps add ipython==8.30.0 2024-12-10 15:25:37 +08:00
Chenyang Shen
c715300416 Merge pull request #14623 from jumpserver/pr@dev@separate_face_module
feat: Separate the face recognition module.
2024-12-09 17:00:34 +08:00
feng
d9031ae02b perf: Ticket filter assignee_id 2024-12-09 16:58:17 +08:00
Aaron3S
0d2ba5c518 feat: Separate the face recognition module. 2024-12-09 16:57:05 +08:00
Bai
817957dbac fix: fixed an issue where auth backend could pass inspect 2024-12-09 15:38:20 +08:00
feng
3796af78a6 perf: Random secret string 2024-12-09 15:25:31 +08:00
github-actions[bot]
1191e4ab2d perf: Update Dockerfile with new base image tag 2024-12-09 14:21:01 +08:00
吴小白
1c6fcc5826 feat: migrating boto to boto3 2024-12-09 14:21:01 +08:00
Chenyang Shen
4728f95634 Merge pull request #14610 from jumpserver/pr@dev@feat_face_login_acl
feat: login asset face verify acl
2024-12-09 11:34:09 +08:00
Aaron3S
013502186b feat: login asset face verify acl 2024-12-09 11:19:04 +08:00
feng
a6d040cd34 perf: Automation filter org 2024-12-06 18:00:56 +08:00
Bai
398758baa6 fix: when oidc enabled and use_state user login raise 400 2024-12-06 16:26:28 +08:00
吴小白
e29bddd89e feat: bump python from 3.11.10 to 3.11.11 2024-12-06 10:29:41 +08:00
Bai
e35c915ee3 perf: add workflows auto release docs 2024-12-06 10:20:24 +08:00
Bryan
de2dd583d0 Update README.zh-hant.md 2024-12-05 14:49:42 +08:00
Bryan
43f1d7eeae Update README.pt-br.md 2024-12-05 14:49:42 +08:00
Bryan
9bb63e0933 Update README.zh-hans.md 2024-12-05 14:49:42 +08:00
Bryan
c9e03fd5d8 Update README.ja.md 2024-12-05 14:49:42 +08:00
github-actions[bot]
7a147242c9 Auto-translate README 2024-12-05 14:49:42 +08:00
github-actions[bot]
392c261a96 Auto-translate README 2024-12-05 14:49:42 +08:00
Bai
2bbccae0f5 perf: readme 2024-12-05 14:39:23 +08:00
Bai
606fa9bfbc feat: change action 2024-12-05 14:24:38 +08:00
fit2bot
96e7b165dd Auto-translate README (#14584)
* Auto-translate README

* Auto-translate README

* Auto-translate README

* Auto-translate README

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 14:17:27 +08:00
Bai
148413d280 feat: add auto translate readme 2024-12-05 13:34:04 +08:00
Bai
a46a81d477 feat: add auto translate readme 2024-12-05 13:34:04 +08:00
feng
ff0f9eb6eb perf: Change secret update version 2024-12-05 10:50:07 +08:00
jiangweidong
d8dfaf0868 fix: Solve the problem of version increase caused by push account 2024-12-04 18:50:30 +08:00
Bai
3267c8074b feat: add actions for translate readme 2024-12-04 17:45:21 +08:00
Bai
7b14d680b2 feat: add actions for translate readme 2024-12-04 17:24:12 +08:00
Bai
0980808bb7 fix: compile languages error 2024-12-03 18:14:42 +08:00
Bai
0519f15bbf fix: compile languages error 2024-12-03 18:07:55 +08:00
Eric
f6742eb4c6 perf: add dbeaver-patch version 2024-12-03 17:36:59 +08:00
fit2bot
f8d11013fc feat: support pt-br language (#14567)
Co-authored-by: Bai <baijiangjie@gmail.com>
2024-12-03 17:11:08 +08:00
jiangweidong
7875777ed1 fix: Resolving Azure test connection failure issues 2024-12-03 14:34:48 +08:00
jiangweidong
0ca81a8f30 fix: To resolve the 500 error during local updates after an account is deleted from Vault 2024-12-03 14:34:07 +08:00
Bryan
09accbd922 perf: update issue template sorted (#14563)
* perf: update issue template sorted

* perf: update issue template sorted

* Rename 1_bug_report_cn.yml to 2_bug_report_cn.yml

* perf: update issue template sorted

* Rename 1_question.yml to 2_question.yml

* Update and rename 2_feature_request.yml to 3_feature_request.yml

* Rename 2_bug_report_cn.yml to 4_bug_report_cn.yml

* Rename 3_question_cn.yml to 5_question_cn.yml

* Update and rename 2_feature_request_cn.yml to 6_feature_request_cn.yml
2024-12-03 11:21:29 +08:00
fit2bot
945204c45b perf: Script to Add a Non-existent release_assets Field (#14558)
* perf: Script to Add a Non-existent release_assets Field

* perf: docstring

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-12-02 16:51:52 +08:00
Bai
2d62dc0657 fix: azure vault max_workers 2024-12-02 11:08:59 +08:00
fit2bot
fa61688c28 feat: Vault adds Amazon Secrets Manager (#14515)
* feat: Vault adds Amazon Secrets Manager

* perf: optimizing the code

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-11-29 17:51:28 +08:00
halo
801edc7cc9 perf: After optimizing the execution of the azure vault task, the data is out of sync 2024-11-29 16:27:35 +08:00
Bai
d0617a0ea4 fix: login log get ipv6 error 2024-11-29 14:59:01 +08:00
Chenyang Shen
1191ed1793 Merge pull request #14546 from jumpserver/pr@dev@feat_move_face_to_profile
feat: move face setiing to profile
2024-11-28 18:19:32 +08:00
Aaron3S
4036420d0e feat: move face setiing to profile 2024-11-28 18:06:57 +08:00
jiangweidong
35a1655905 perf: Oauth2.0 support two methods for passing authentication credentials. 2024-11-26 14:12:56 +08:00
feng
d4dc31aefa perf: Modify default_context COPYRIGHT 2024-11-25 15:30:26 +08:00
wangruidong
04ec34364f perf: Add viewAssetOnlineSessionInfo conf 2024-11-25 15:28:57 +08:00
Aaron3S
01b8c1f7a8 fix: Fix the uncaught exception when face capture fails 2024-11-25 10:17:28 +08:00
Bai
77598a0f23 perf: update readme 2024-11-22 16:43:44 +08:00
wangruidong
eafb074fda refactor: API endpoint 2024-11-22 15:14:26 +08:00
Bryan
d4d903f5c6 perf: Update README.md (#14516)
* perf: Update README.md

* perf: Update README.md
2024-11-22 10:38:32 +08:00
吴小白
c9c55b5fcb fix: add libldap2-dev 2024-11-21 20:53:11 +08:00
Bryan
25987545db Merge pull request #14511 from jumpserver/dev
v4.4.0
2024-11-21 19:00:35 +08:00
wangruidong
f7313bfcc1 perf: Audits job api disable periodic task 2024-11-21 18:56:16 +08:00
Bai
d2f7376f78 fix: job execution stop failed 2024-11-21 18:38:10 +08:00
wangruidong
6db56eb2aa fix: view ops job celery log no perms 2024-11-21 18:14:45 +08:00
fit2bot
442290703a fix: pyfreerdp verify account, the default value of gateway_args field is wrong (#14490)
* fix: pyfreerdp verify account, the default value of gateway_args field is wrong

* fix: pyfreerdp verify account, the default value of gateway_args field is wrong

---------

Co-authored-by: Ewall555 <a03216@foxmail.com>
2024-11-21 14:26:22 +08:00
feng
e491a724ed perf: Video player download 2024-11-21 14:25:50 +08:00
feng
230924baac fix: Vault proxy 2024-11-21 13:40:33 +08:00
wangruidong
0ae2f04f28 fix: view ops job celery log no perms 2024-11-21 13:24:29 +08:00
feng
68a490d305 perf: Hide azure vault 2024-11-21 13:02:40 +08:00
wangruidong
6abfeee683 feat: Add periodic display and validate job params 2024-11-20 22:07:56 +08:00
Aaron3S
1a03f7b265 feat: add license edition check 2024-11-20 20:09:11 +08:00
feng
2dae2b3789 perf: Translate 2024-11-20 18:21:31 +08:00
Aaron3S
bdbbebab76 feat: perf face capture page 2024-11-20 17:54:27 +08:00
Chenyang Shen
33170887f4 Merge pull request #14495 from jumpserver/pr@dev@feat_add_check_api_white_list
feat: add 'face_context' to check_api white list
2024-11-20 17:52:06 +08:00
Aaron3S
88302c8846 feat: add 'face_context' to check_api white list 2024-11-20 16:38:22 +08:00
feng
4068b5c76a perf: Change secret ssh_key_change_strategy modify the default value 2024-11-20 16:27:21 +08:00
feng
9966ad4c71 perf: Dynamic update vault 2024-11-20 15:58:20 +08:00
Aaron3S
9cfe974c52 feat: 添加 mfa middleware 白名单 2024-11-20 14:18:52 +08:00
feng
d9a9f890f5 perf: Lina AzureKeyVault translate 2024-11-20 14:08:27 +08:00
fit2bot
e2904ab042 perf: Custom SMS (files) support obtaining more user information. (#14486)
* perf: Custom SMS (files) support obtaining more user information.

* perf: Remove the useless modules

* perf: modify

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-11-20 10:29:14 +08:00
Aaron3S
f92c557235 feat: 增加人脸识别超时控制 2024-11-20 10:27:04 +08:00
halo
cfadbc164c perf: If the cloud vault initialization fails, the task will not be executed. 2024-11-20 10:15:14 +08:00
feng
374a102bc4 perf: Translate 2024-11-19 18:58:43 +08:00
feng
84e1411c22 fix: Clone endpoint 500 2024-11-19 18:09:00 +08:00
wangruidong
e28bf170d1 perf: MFA Translate 2024-11-19 17:55:11 +08:00
wangruidong
7c9e3a1362 perf: Optimize summary calculation 2024-11-19 17:55:11 +08:00
feng
fba80342a5 perf: Translate 2024-11-19 17:54:45 +08:00
Aaron3S
5eeff0aabf feat: 设置人脸上下文存活时间 2024-11-19 17:34:44 +08:00
Aaron3S
5b4de02fff feat: 增加绑定成功失败提示 2024-11-19 17:30:31 +08:00
wangruidong
b6a5854fa2 perf: Optimize summary calculation 2024-11-19 16:13:38 +08:00
Chenyang Shen
9771d3c817 Merge pull request #14476 from jumpserver/pr@dev@feat_add_face_i18n
FEAT: Add face recognition translation
2024-11-19 15:11:03 +08:00
Aaron3S
b33a0cf0b1 feat: 添加人脸识别翻译 2024-11-19 15:08:39 +08:00
Chenyang Shen
f9fa6ad9c1 Merge pull request #14474 from jumpserver/pr@dev@feat_update_face_capture_page
feat: Optimized the face collection page
2024-11-19 15:01:45 +08:00
Aaron3S
4b2db2b6a1 feat: 优化人脸采集页面 2024-11-19 14:28:31 +08:00
Halo
822b353a40 perf: Translate (#14468)
* feat: azure key vault

* perf: add azure-keyvault-secrets

* perf:azure kv api

* perf: Translate

* perf: Update Dockerfile with new base image tag

* perf: Error when secret is empty

* perf: Translate

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-18 18:48:33 +08:00
feng
2908d4ee5f perf: Delete asset fail 2024-11-18 10:08:37 +08:00
wangruidong
482c4ced0c perf: Translate 2024-11-15 18:00:35 +08:00
halo
b2a5e457a9 fix: vault synchronization task exception 2024-11-15 17:46:17 +08:00
wangruidong
343c3607fa fix: modify job audit rbac 2024-11-15 15:47:20 +08:00
wangruidong
f03263eedf fix: Radius login failed 2024-11-15 15:44:05 +08:00
Aaron3S
98d7ecbf3e fix: 修改错误的url地址 2024-11-13 17:35:43 +08:00
halo
477ccda8ca perf: VAULT_BACKEND cannot be modified from the frontend 2024-11-13 17:31:47 +08:00
wangruidong
fcdc2b9510 fix: Solve audit job and variable bugs 2024-11-13 17:31:17 +08:00
wangruidong
1ee57cfda0 perf: ticket info add org name 2024-11-12 18:15:31 +08:00
wangruidong
804bd289a4 fix: Other people can delete adhoc or playbook 2024-11-12 17:44:21 +08:00
Aaron3S
86273865c8 feat: 增加人脸识别功能 2024-11-12 17:41:39 +08:00
Eric
5142f0340c perf: add license info for component config 2024-11-12 16:52:45 +08:00
Bai
7c80c52d02 fix: Set the default language to en 2024-11-12 15:43:57 +08:00
Bai
eb30b61ca9 fix: Set the default language to en 2024-11-12 15:38:01 +08:00
wangruidong
dd5a272cdf perf: Add task handler for ops job with creator assignment 2024-11-12 15:16:01 +08:00
wangruidong
5b27acf4ef perf: Admin and auditor can view and stop task 2024-11-12 11:25:12 +08:00
Eric
1a41a7450e perf: vnc proxy port to 15900 2024-11-11 19:46:24 +08:00
fit2bot
e1b501c7d4 feat: azure key vault (#14406)
* feat: azure key vault

* perf: add azure-keyvault-secrets

* perf:azure kv api

* perf: Translate

* perf: Update Dockerfile with new base image tag

* perf: Error when secret is empty

* perf: Translate

---------

Co-authored-by: halo <wuyihuangw@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-11 19:41:47 +08:00
Eric
b660bfb7ff perf: add nec vnc component and endpoint migrations 2024-11-11 18:54:52 +08:00
wangruidong
5724912480 perf: Add check for SECURITY_COMMAND_EXECUTION settings in ops tasks 2024-11-11 18:14:43 +08:00
feng
11b3bafd5a perf: Translate 2024-11-08 15:34:37 +08:00
wangruidong
9f90838df1 perf: Optimize username handling in push_account 2024-11-07 10:47:01 +08:00
wangruidong
b01916001e fix: User import fails if MFA field is set to Disabled (0) 2024-11-07 10:45:05 +08:00
fit2bot
c96ae1022b feat: Supports running adhoc,playbook with variable (#14417)
* perf:Create a job that supports adding node parameters

* feat: add variable model

* feat: Modify Variable and AdHoc models,

* feat: Parameters can be set when running job

* feat: Supports setting  variable type

* feat: Supports running adhoc with parameters

* feat: Supports running playbook with parameters

* fix: Translate

* feat: Support setting variables for scheduled tasks

* perf: Translate

---------

Co-authored-by: wangruidong <940853815@qq.com>
2024-11-07 10:38:34 +08:00
jiangweidong
8f11167db0 perf: i18n - Supports automatic release of assets and prevents accidental release of network errors 2024-11-06 15:07:26 +08:00
老广
a53397b76f Update llm-code-review.yml 2024-11-05 18:20:38 +08:00
老广
8f13224454 Create llm-code-review.yml 2024-11-04 18:34:06 +08:00
Bai
8f4dd25e69 feat: DEFAULT_EXPIRED_YEARS put in public settings API 2024-11-01 18:24:54 +08:00
Bai
9c8762e3a0 feat: support configuration DEFAULT_EXPIRED_YEARS 2024-11-01 15:48:57 +08:00
Bai
a8cf788122 feat: add GitHub Action to automatically publish release notes to Discord changelog channel. 2024-11-01 15:24:07 +08:00
Bai
7355a4f152 feat: add GitHub Action to automatically publish release notes to Discord changelog channel. 2024-11-01 14:21:48 +08:00
ibuler
2cf80e6615 perf: login success to call client 2024-10-31 18:36:42 +08:00
ibuler
9a18ed631c fix: oracle platform create error 2024-10-30 16:33:18 +08:00
Bai
1e16f1cb9f fix: console dashboard proportion describe 2024-10-29 19:09:50 +08:00
fit2bot
35b8b080ab perf: add to cron.d (#14375)
* perf: add to cron.d

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 16:15:07 +08:00
ibuler
4219d54db3 perf: add cron in dockerfile 2024-10-29 15:39:54 +08:00
ibuler
c3620254b3 perf: change docker file 2024-10-29 15:32:57 +08:00
fit2bot
d30de0b6a0 perf: update chrome applets hang (#14353)
* perf: update chrome applets hang

* perf: remove debug print

---------

Co-authored-by: Eric <xplzv@126.com>
2024-10-29 15:19:15 +08:00
github-actions[bot]
af91b6faeb perf: Update Dockerfile with new base image tag 2024-10-29 15:18:24 +08:00
ibuler
49b84b019d perf: using poetry mirror 2024-10-29 15:18:24 +08:00
ibuler
a0ee520572 perf: remove cache 2024-10-29 15:18:24 +08:00
fit2bot
972afe0bfe perf: revert old deps (#14371)
* perf: revert old deps

* perf: update poetry

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 14:24:31 +08:00
wangruidong
e47e9b0a11 fix: Unique basename 2024-10-29 11:38:46 +08:00
fit2bot
87e54d8823 perf: add cron (#14364)
* perf: add cron

* Update Dockerfile-base

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 10:56:42 +08:00
jiangweidong
a73c8d8285 fix: Wechat ticket message some url cannot clicked 2024-10-25 15:05:13 +08:00
Eric
b0dd8d044d perf: add error msg when applet task failed 2024-10-24 14:55:33 +08:00
jiangweidong
7c55c42582 perf: Links in WeCom messages can be opened without re-logging in. 2024-10-22 17:02:59 +08:00
fit2bot
cc1fcd2b98 perf: move storage sdk to core (#14318)
* perf: move storage sdk to core

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <baijiangjie@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 15:20:10 +08:00
fit2bot
8434d8d5ba perf: update dependency (#14307)
* perf: update dependency

* perf: remove source build

* perf: Update Dockerfile with new base image tag

* perf: use cache build

* perf: Update Dockerfile with new base image tag

* fix: variable incorrectly defined

* perf: Update Dockerfile with new base image tag

* fix: openpyxl fixed version

* perf: Update Dockerfile with new base image tag

* perf: remove cache

* perf: Update Dockerfile with new base image tag

* perf: update pyproject.toml

* perf: Update Dockerfile with new base image tag

* perf: remove cache

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: 吴小白 <296015668@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 14:16:19 +08:00
feng
044fd238b8 perf: Remove ssh_key_change_strategy add value 2024-10-21 15:25:38 +08:00
feng
be096a1319 perf: List preference translate 2024-10-18 14:36:13 +08:00
吴小白
6fa14833b3 perf: use python embed 2024-10-18 11:02:49 +08:00
feng
1f32ab274c fix: Error subpub_msg log 2024-10-17 15:17:32 +08:00
Bryan
6720ecc6e0 Merge pull request #14319 from jumpserver/dev
v4.3.0
2024-10-17 14:55:38 +08:00
feng
b0f86e43a6 perf: Translate 2024-10-17 12:05:25 +08:00
ibuler
9b0c81333f perf: debug pub sub 2024-10-17 10:16:44 +08:00
Eric
05fc966444 perf: add koko i18n 2024-10-16 18:25:42 +08:00
Eric
b87650038f perf: update code 2024-10-16 18:11:00 +08:00
wangruidong
d4f69a7ff8 perf: Translate 2024-10-16 17:59:18 +08:00
ibuler
0e1e26c29c perf: disable f1 key 2024-10-16 17:01:10 +08:00
Huaqing Chen
1b8cdbc4dd 修复websocket不能使用Authorization Header的问题 2024-10-15 14:13:38 +08:00
feng
2a781c228f perf: Cas user cannot bind organization 2024-10-15 10:50:20 +08:00
ZhaoJiSen
35d6b0f16a Merge pull request #14299 from jumpserver/pr@dev@change_password_length
perf: Change secret remove redundant checks
2024-10-14 16:45:27 +08:00
feng
ca8987fef6 perf: Change secret remove redundant checks 2024-10-14 16:39:31 +08:00
ZhaoJiSen
b385133071 Merge pull request #14297 from jumpserver/pr@dev@translate
perf: Translate
2024-10-14 16:09:21 +08:00
feng
aa78a03efa perf: Translate 2024-10-14 16:05:38 +08:00
wangruidong
31f8a19392 perf: Translate account history 2024-10-14 15:31:17 +08:00
wangruidong
7a528b499a perf: import data validate platform 2024-10-14 14:05:24 +08:00
Eric
1c6ce422cf perf: update tinker v0.1.9 2024-10-12 16:30:28 +08:00
Eric
f9cf2ea2e5 perf: fix api error when deleting offline panda components 2024-10-12 16:15:23 +08:00
Aaron3S
575b3a617f feat: 添加 chen 翻译 2024-10-12 15:44:38 +08:00
wangruidong
b7362d3f51 fix: adhoc execute alert msg 2024-10-12 15:43:03 +08:00
ZhaoJiSen
6ee3860124 Merge pull request #14287 from jumpserver/pr@dev@translate
perf: Translate
2024-10-12 14:40:23 +08:00
feng
7e111da529 perf: Translate 2024-10-12 14:35:18 +08:00
wangruidong
578458f734 perf: site msg content optimize 2024-10-11 11:28:56 +08:00
Bai
bd56697d6d perf: DEFAULT_PAGE_SIZE same as MAX_LIMIT_PER_PAGE 2024-10-10 18:00:01 +08:00
wangruidong
aad824d127 perf: add created_by field 2024-10-09 16:14:22 +08:00
wangruidong
63f828da0b perf: Default endpoint cannot be disabled 2024-10-09 16:12:37 +08:00
wangruidong
7c211b3fb6 perf: Translate 2024-10-08 15:01:53 +08:00
feng
3881edd2ba perf: Optimize file audit download prompt 2024-09-29 16:12:49 +08:00
feng
b882b12d04 perf: Check the validity of the connection token 2024-09-27 17:10:08 +08:00
wangruidong
addd2e7d1c perf: Endpoint add is_active field 2024-09-27 16:00:05 +08:00
Bai
ad6d2e1cd7 fix: Fixed the issue that the workbench user login log only displays failed logs 2024-09-27 14:34:23 +08:00
github-actions[bot]
5f07271afa perf: Update Dockerfile with new base image tag 2024-09-27 14:30:48 +08:00
Bai
efdcd4c708 perf: upgrade geoip2 and .mmdb 2024-09-27 14:30:48 +08:00
jiangweidong
b62763bca3 perf: Cloud Sync IP Policy Updated to Preferred Option i18n 2024-09-27 14:29:09 +08:00
wangruidong
e95da730f2 perf: Koko can display assets custom name 2024-09-27 14:25:55 +08:00
fit2bot
43fa3f420a fix: Addressing the issue of unauthorized execution of system tools (#14209)
* fix: Addressing the issue of unauthorized execution of system tools

* perf: Optimization conditions

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-09-27 14:17:16 +08:00
wangruidong
0311446384 perf: playbook clone with file 2024-09-27 14:13:35 +08:00
feng
f7030e4fee perf: Login encryption key cache added 2024-09-26 15:11:35 +08:00
ZhaoJiSen
fce8cc375f Merge pull request #14230 from jumpserver/pr@dev@max_password_length
perf: The maximum length of the randomly generated password is changed to 36
2024-09-25 11:00:45 +08:00
feng
920199c6df perf: The maximum length of the randomly generated password is changed to 36 2024-09-25 10:52:16 +08:00
feng
d09eb3c4fa perf: Lock username is not case sensitive 2024-09-23 14:11:55 +08:00
ibuler
6e8affcdd6 perf: ops db migrate 2024-09-19 21:39:55 +08:00
314 changed files with 24440 additions and 7984 deletions

24
.github/workflows/discord-release.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Publish Release to Discord
on:
release:
types: [published]
jobs:
send_discord_notification:
runs-on: ubuntu-latest
if: startsWith(github.event.release.tag_name, 'v4.')
steps:
- name: Send release notification to Discord
env:
WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }}
run: |
# 获取标签名称和 release body
TAG_NAME="${{ github.event.release.tag_name }}"
RELEASE_BODY="${{ github.event.release.body }}"
# 使用 jq 构建 JSON 数据,以确保安全传递
JSON_PAYLOAD=$(jq -n --arg tag "# JumpServer $TAG_NAME Released! 🚀" --arg body "$RELEASE_BODY" '{content: "\($tag)\n\($body)"}')
# 使用 curl 发送 JSON 数据
curl -X POST -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"

24
.github/workflows/docs-release.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Auto update docs changelog
on:
release:
types: [published]
jobs:
update_docs_changelog:
runs-on: ubuntu-latest
if: startsWith(github.event.release.tag_name, 'v4.')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Update docs changelog
env:
TAG_NAME: ${{ github.event.release.tag_name }}
DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}
run: |
git config --global user.name 'BaiJiangJie'
git config --global user.email 'jiangjie.bai@fit2cloud.com'
git clone https://$DOCS_TOKEN@github.com/jumpservice/documentation.git
cd documentation/utils
bash update_changelog.sh

28
.github/workflows/llm-code-review.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2-1.5b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"

40
.github/workflows/translate-readme.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Translate README
on:
workflow_dispatch:
inputs:
target_langs:
description: "Target Languages"
required: false
default: "zh-hans,zh-hant,ja,pt-br"
gen_dir_path:
description: "Generate Dir Name"
required: false
default: "readmes/"
push_branch:
description: "Push Branch"
required: false
default: "pr@dev@translate_readme"
prompt:
description: "AI Translate Prompt"
required: false
default: ""
gpt_mode:
description: "GPT Mode"
required: false
default: "gpt-4o-mini"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Auto Translate
uses: jumpserver-dev/action-translate-readme@main
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }}
GPT_MODE: ${{ github.event.inputs.gpt_mode }}
TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }}
PUSH_BRANCH: ${{ github.event.inputs.push_branch }}
GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }}
PROMPT: ${{ github.event.inputs.prompt }}

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ test.py
.history/
.test/
*.mo
apps.iml

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20240919_024156 AS stage-build
FROM jumpserver/core-base:20241210_070105 AS stage-build
ARG VERSION
@@ -28,6 +28,7 @@ ARG DEPENDENCIES=" \
libx11-dev"
ARG TOOLS=" \
cron \
ca-certificates \
default-libmysqlclient-dev \
openssh-client \
@@ -35,19 +36,20 @@ ARG TOOLS=" \
bubblewrap"
ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update > /dev/null \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& apt-get clean \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "no" | dpkg-reconfigure dash \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/* \
&& echo "0 3 * * * root find /tmp -type f -mtime +1 -size +1M -exec rm -f {} \; && date > /tmp/clean.log" > /etc/cron.d/cleanup_tmp \
&& chmod 0644 /etc/cron.d/cleanup_tmp
COPY --from=stage-build /opt /opt
COPY --from=stage-build /usr/local/bin /usr/local/bin

View File

@@ -15,8 +15,8 @@ ARG DEPENDENCIES=" \
libldap2-dev \
libsasl2-dev"
ARG APT_MIRROR=http://deb.debian.org
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
@@ -27,9 +27,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
# Install bin tools
ARG CHECK_VERSION=v1.0.3
ARG CHECK_VERSION=v1.0.4
RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
@@ -38,23 +37,24 @@ RUN set -ex \
&& chmod 755 /usr/local/bin/check \
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
# Install Python dependencies
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=poetry.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& . /opt/py3/bin/activate \
&& poetry install --only main \
&& poetry config virtualenvs.create false \
&& poetry install --no-cache --only main \
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
&& bash clean_site_packages.sh
&& bash clean_site_packages.sh \
&& poetry cache clear pypi --all

View File

@@ -15,21 +15,20 @@ ARG TOOLS=" \
vim \
wget"
ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& echo "no" | dpkg-reconfigure dash
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
COPY poetry.lock pyproject.toml ./
RUN set -ex \
&& . /opt/py3/bin/activate \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry install --only xpack
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& poetry install --only xpack \
&& poetry cache clear pypi --all

View File

@@ -10,7 +10,8 @@
[![][github-release-shield]][github-release-link]
[![][github-stars-shield]][github-stars-link]
**English** · [简体中文](./README.zh-CN.md)
[English](/README.md) · [中文(简体)](/readmes/README.zh-hans.md) · [中文(繁體)](/readmes/README.zh-hant.md) · [日本語](/readmes/README.ja.md) · [Português (Brasil)](/readmes/README.pt-br.md)
</div>
<br/>
@@ -68,10 +69,13 @@ JumpServer consists of multiple key components, which collectively form the func
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector |
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
| [Nec](https://github.com/jumpserver/nec) | <img alt="Nec" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE VNC Proxy Connector |
| [Facelive](https://github.com/jumpserver/facelive) | <img alt="Facelive" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Facial Recognition |
## Contributing
@@ -85,7 +89,7 @@ JumpServer is a mission critical product. Please refer to the Basic Security Rec
## License
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

View File

@@ -109,7 +109,7 @@ JumpServer是一款安全产品请参考 [基本安全建议](https://docs.ju
## License & Copyright
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2024 飞致云, All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at

View File

@@ -30,6 +30,6 @@
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default(None) }}"
gateway_args: "{{ jms_gateway | default({}) }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -160,6 +160,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
ChangeSecretRecord.objects.bulk_create(records)
return inventory_hosts
@staticmethod
def require_update_version(account, recorder):
return account.secret != recorder.new_secret
def on_host_success(self, host, result):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
@@ -171,6 +175,8 @@ class ChangeSecretManager(AccountBasePlaybookManager):
if not account:
print("Account not found, deleted ?")
return
version_update_required = self.require_update_version(account, recorder)
account.secret = recorder.new_secret
account.date_updated = timezone.now()
@@ -180,7 +186,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
while retry_count < max_retries:
try:
recorder.save()
account.save(update_fields=['secret', 'version', 'date_updated'])
account_update_fields = ['secret', 'date_updated']
if version_update_required:
account_update_fields.append('version')
account.save(update_fields=account_update_fields)
break
except Exception as e:
retry_count += 1

View File

@@ -30,6 +30,6 @@
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default(None) }}"
gateway_args: "{{ jms_gateway | default({}) }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -8,6 +8,11 @@ logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
@staticmethod
def require_update_version(account, recorder):
account.skip_history_when_saving = True
return False
@classmethod
def method_type(cls):
return AutomationTypes.push_account

View File

@@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from assets.automations.ping_gateway.manager import PingGatewayManager
from common.utils import get_logger
@@ -13,7 +15,7 @@ class VerifyGatewayAccountManager(PingGatewayManager):
@staticmethod
def before_runner_start():
logger.info(">>> 开始执行测试网关账号可连接性任务")
logger.info(_(">>> Start executing the task to test gateway account connectivity"))
def get_accounts(self, gateway):
account_ids = self.execution.snapshot['accounts']

View File

@@ -1,19 +1,18 @@
from importlib import import_module
from django.utils.functional import LazyObject
from django.utils.functional import LazyObject, empty
from common.utils import get_logger
from ..const import VaultTypeChoices
__all__ = ['vault_client', 'get_vault_client']
__all__ = ['vault_client', 'get_vault_client', 'refresh_vault_client']
logger = get_logger(__file__)
def get_vault_client(raise_exception=False, **kwargs):
enabled = kwargs.get('VAULT_ENABLED')
tp = 'hcp' if enabled else 'local'
tp = kwargs.get('VAULT_BACKEND') if kwargs.get('VAULT_ENABLED') else VaultTypeChoices.local
try:
module_path = f'apps.accounts.backends.{tp}.main'
client = import_module(module_path).Vault(**kwargs)
@@ -39,3 +38,7 @@ class VaultClient(LazyObject):
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
vault_client = VaultClient()
def refresh_vault_client():
vault_client._wrapped = empty

View File

@@ -0,0 +1 @@
from .main import *

View File

@@ -0,0 +1,16 @@
from .service import AmazonSecretsManagerClient
from ..base.vault import BaseVault
from ..utils.mixins import GeneralVaultMixin
from ...const import VaultTypeChoices
class Vault(GeneralVaultMixin, BaseVault):
type = VaultTypeChoices.aws
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = AmazonSecretsManagerClient(
region_name=kwargs.get('VAULT_AWS_REGION_NAME'),
access_key_id=kwargs.get('VAULT_AWS_ACCESS_KEY_ID'),
secret_key=kwargs.get('VAULT_AWS_ACCESS_SECRET_KEY'),
)

View File

@@ -0,0 +1,56 @@
import boto3
from common.utils import get_logger, random_string
logger = get_logger(__name__)
__all__ = ['AmazonSecretsManagerClient']
class AmazonSecretsManagerClient(object):
def __init__(self, region_name, access_key_id, secret_key):
self.client = boto3.client(
'secretsmanager', region_name=region_name,
aws_access_key_id=access_key_id, aws_secret_access_key=secret_key,
)
self.empty_secret = '#{empty}#'
def is_active(self):
try:
secret_id = f'jumpserver/test-{random_string(12)}'
self.create(secret_id, 'secret')
self.get(secret_id)
self.update(secret_id, 'secret')
self.delete(secret_id)
except Exception as e:
return False, f'Vault is not reachable: {e}'
else:
return True, ''
def get(self, name, version=''):
params = {'SecretId': name}
if version:
params['VersionStage'] = version
try:
secret = self.client.get_secret_value(**params)['SecretString']
return secret if secret != self.empty_secret else ''
except Exception: # noqa
return ''
def create(self, name, secret):
self.client.create_secret(Name=name, SecretString=secret or self.empty_secret)
def update(self, name, secret):
self.client.update_secret(SecretId=name, SecretString=secret or self.empty_secret)
def delete(self, name):
self.client.delete_secret(SecretId=name)
def update_metadata(self, name, metadata: dict):
tags = [{'Key': k, 'Value': v} for k, v in metadata.items()]
try:
self.client.tag_resource(SecretId=name, Tags=tags)
except Exception as e:
logger.error(f'update_metadata: {name} {str(e)}')

View File

@@ -0,0 +1 @@
from .main import *

View File

@@ -0,0 +1,33 @@
from ..base.entries import BaseEntry
class AzureBaseEntry(BaseEntry):
@property
def full_path(self):
return self.path_spec
class AccountEntry(AzureBaseEntry):
@property
def path_spec(self):
# 长度 0-127
account_id = str(self.instance.id)[:18]
path = f'assets-{self.instance.asset_id}-accounts-{account_id}'
return path
class AccountTemplateEntry(AzureBaseEntry):
@property
def path_spec(self):
path = f'account-templates-{self.instance.id}'
return path
class HistoricalAccountEntry(AzureBaseEntry):
@property
def path_spec(self):
path = f'accounts-{self.instance.instance.id}-histories-{self.instance.history_id}'
return path

View File

@@ -0,0 +1,17 @@
from .service import AZUREVaultClient
from ..base.vault import BaseVault
from ..utils.mixins import GeneralVaultMixin
from ...const import VaultTypeChoices
class Vault(GeneralVaultMixin, BaseVault):
type = VaultTypeChoices.azure
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = AZUREVaultClient(
vault_url=kwargs.get('VAULT_AZURE_HOST'),
tenant_id=kwargs.get('VAULT_AZURE_TENANT_ID'),
client_id=kwargs.get('VAULT_AZURE_CLIENT_ID'),
client_secret=kwargs.get('VAULT_AZURE_CLIENT_SECRET')
)

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
#
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from common.utils import get_logger
logger = get_logger(__name__)
__all__ = ['AZUREVaultClient']
class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret):
authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
credentials = ClientSecretCredential(
client_id=client_id, client_secret=client_secret, tenant_id=tenant_id, authority=authentication_endpoint
)
self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self):
try:
self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(str(e))
return False, f'Vault is not reachable: {e}'
else:
return True, ''
def get(self, name, version=None):
try:
secret = self.client.get_secret(name, version)
return secret.value
except (ResourceNotFoundError, ClientAuthenticationError) as e:
return ''
def create(self, name, secret):
if not secret:
secret = ''
self.client.set_secret(name, secret)
def update(self, name, secret):
if not secret:
secret = ''
self.client.set_secret(name, secret)
def delete(self, name):
self.client.begin_delete_secret(name)
def update_metadata(self, name, metadata: dict):
try:
self.client.update_secret_properties(name, tags=metadata)
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(f'update_metadata: {name} {str(e)}')

View File

@@ -1,74 +0,0 @@
from abc import ABC, abstractmethod
from django.forms.models import model_to_dict
__all__ = ['BaseVault']
class BaseVault(ABC):
def __init__(self, *args, **kwargs):
self.enabled = kwargs.get('VAULT_ENABLED')
def get(self, instance):
""" 返回 secret 值 """
return self._get(instance)
def create(self, instance):
if not instance.secret_has_save_to_vault:
self._create(instance)
self._clean_db_secret(instance)
self.save_metadata(instance)
if instance.is_sync_metadata:
self.save_metadata(instance)
def update(self, instance):
if not instance.secret_has_save_to_vault:
self._update(instance)
self._clean_db_secret(instance)
self.save_metadata(instance)
if instance.is_sync_metadata:
self.save_metadata(instance)
def delete(self, instance):
self._delete(instance)
def save_metadata(self, instance):
metadata = model_to_dict(instance, fields=[
'name', 'username', 'secret_type',
'connectivity', 'su_from', 'privileged'
])
metadata = {k: str(v)[:500] for k, v in metadata.items() if v}
return self._save_metadata(instance, metadata)
# -------- abstractmethod -------- #
@abstractmethod
def _get(self, instance):
raise NotImplementedError
@abstractmethod
def _create(self, instance):
raise NotImplementedError
@abstractmethod
def _update(self, instance):
raise NotImplementedError
@abstractmethod
def _delete(self, instance):
raise NotImplementedError
@abstractmethod
def _clean_db_secret(self, instance):
raise NotImplementedError
@abstractmethod
def _save_metadata(self, instance, metadata):
raise NotImplementedError
@abstractmethod
def is_active(self, *args, **kwargs) -> (bool, str):
raise NotImplementedError

View File

View File

@@ -1,19 +1,18 @@
import sys
from abc import ABC
from common.db.utils import Encryptor
from common.utils import lazyproperty
current_module = sys.modules[__name__]
__all__ = ['build_entry']
class BaseEntry(ABC):
def __init__(self, instance):
self.instance = instance
@property
def path_base(self):
path = f'orgs/{self.instance.org_id}'
return path
@lazyproperty
def full_path(self):
path_base = self.path_base
@@ -21,32 +20,24 @@ class BaseEntry(ABC):
path = f'{path_base}/{path_spec}'
return path
@property
def path_base(self):
path = f'orgs/{self.instance.org_id}'
return path
@property
def path_spec(self):
raise NotImplementedError
def to_internal_data(self):
def get_encrypt_secret(self):
secret = getattr(self.instance, '_secret', None)
if secret is not None:
secret = Encryptor(secret).encrypt()
data = {'secret': secret}
return data
return secret
@staticmethod
def to_external_data(data):
secret = data.pop('secret', None)
def get_decrypt_secret(secret):
if secret is not None:
secret = Encryptor(secret).decrypt()
return secret
class AccountEntry(BaseEntry):
@property
def path_spec(self):
path = f'assets/{self.instance.asset_id}/accounts/{self.instance.id}'
@@ -54,7 +45,6 @@ class AccountEntry(BaseEntry):
class AccountTemplateEntry(BaseEntry):
@property
def path_spec(self):
path = f'account-templates/{self.instance.id}'
@@ -62,23 +52,12 @@ class AccountTemplateEntry(BaseEntry):
class HistoricalAccountEntry(BaseEntry):
@property
def path_base(self):
account = self.instance.instance
path = f'accounts/{account.id}/'
path = f'accounts/{self.instance.instance.id}'
return path
@property
def path_spec(self):
path = f'histories/{self.instance.history_id}'
return path
def build_entry(instance) -> BaseEntry:
class_name = instance.__class__.__name__
entry_class_name = f'{class_name}Entry'
entry_class = getattr(current_module, entry_class_name, None)
if not entry_class:
raise Exception(f'Entry class {entry_class_name} is not found')
return entry_class(instance)

View File

@@ -0,0 +1,109 @@
import importlib
import inspect
from abc import ABC, abstractmethod
from django.forms.models import model_to_dict
from .entries import BaseEntry
from ...const import VaultTypeChoices
class BaseVault(ABC):
def __init__(self, *args, **kwargs):
self.enabled = kwargs.get('VAULT_ENABLED')
self._entry_classes = {}
self._load_entries()
def _load_entries_import_module(self, module_name):
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
self._entry_classes.setdefault(name, obj)
def _load_entries(self):
if self.type == VaultTypeChoices.local:
return
module_name = f'accounts.backends.{self.type}.entries'
if importlib.util.find_spec(module_name): # noqa
self._load_entries_import_module(module_name)
base_module = 'accounts.backends.base.entries'
self._load_entries_import_module(base_module)
@property
@abstractmethod
def type(self):
raise NotImplementedError
def get(self, instance):
""" 返回 secret 值 """
return self._get(self.build_entry(instance))
def create(self, instance):
if not instance.secret_has_save_to_vault:
entry = self.build_entry(instance)
self._create(entry)
self._clean_db_secret(instance)
self.save_metadata(entry)
def update(self, instance):
entry = self.build_entry(instance)
if not instance.secret_has_save_to_vault:
self._update(entry)
self._clean_db_secret(instance)
self.save_metadata(entry)
if instance.is_sync_metadata:
self.save_metadata(entry)
def delete(self, instance):
entry = self.build_entry(instance)
self._delete(entry)
def save_metadata(self, entry):
metadata = model_to_dict(entry.instance, fields=[
'name', 'username', 'secret_type',
'connectivity', 'su_from', 'privileged'
])
metadata = {k: str(v)[:500] for k, v in metadata.items() if v}
return self._save_metadata(entry, metadata)
def build_entry(self, instance):
if self.type == VaultTypeChoices.local:
return BaseEntry(instance)
entry_class_name = f'{instance.__class__.__name__}Entry'
entry_class = self._entry_classes.get(entry_class_name)
if not entry_class:
raise Exception(f'Entry class {entry_class_name} is not found')
return entry_class(instance)
def _clean_db_secret(self, instance):
instance.is_sync_metadata = False
instance.mark_secret_save_to_vault()
# -------- abstractmethod -------- #
@abstractmethod
def _get(self, instance):
raise NotImplementedError
@abstractmethod
def _create(self, entry):
raise NotImplementedError
@abstractmethod
def _update(self, entry):
raise NotImplementedError
@abstractmethod
def _delete(self, entry):
raise NotImplementedError
@abstractmethod
def _save_metadata(self, instance, metadata):
raise NotImplementedError
@abstractmethod
def is_active(self, *args, **kwargs) -> (bool, str):
raise NotImplementedError

View File

@@ -1,14 +1,18 @@
from common.db.utils import get_logger
from .entries import build_entry
from .service import VaultKVClient
from ..base import BaseVault
from ..base.vault import BaseVault
from ...const import VaultTypeChoices
__all__ = ['Vault']
logger = get_logger(__name__)
__all__ = ['Vault']
class Vault(BaseVault):
type = VaultTypeChoices.hcp
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = VaultKVClient(
@@ -20,34 +24,25 @@ class Vault(BaseVault):
def is_active(self):
return self.client.is_active()
def _get(self, instance):
entry = build_entry(instance)
def _get(self, entry):
# TODO: get data 是不是层数太多了
data = self.client.get(path=entry.full_path).get('data', {})
data = entry.to_external_data(data)
data = entry.get_decrypt_secret(data.get('secret'))
return data
def _create(self, instance):
entry = build_entry(instance)
data = entry.to_internal_data()
def _create(self, entry):
data = {'secret': entry.get_encrypt_secret()}
self.client.create(path=entry.full_path, data=data)
def _update(self, instance):
entry = build_entry(instance)
data = entry.to_internal_data()
def _update(self, entry):
data = {'secret': entry.get_encrypt_secret()}
self.client.patch(path=entry.full_path, data=data)
def _delete(self, instance):
entry = build_entry(instance)
def _delete(self, entry):
self.client.delete(path=entry.full_path)
def _clean_db_secret(self, instance):
instance.is_sync_metadata = False
instance.mark_secret_save_to_vault()
def _save_metadata(self, instance, metadata):
def _save_metadata(self, entry, metadata):
try:
entry = build_entry(instance)
self.client.update_metadata(path=entry.full_path, metadata=metadata)
except Exception as e:
logger.error(f'save metadata error: {e}')

View File

@@ -1,5 +1,6 @@
from common.utils import get_logger
from ..base import BaseVault
from ..base.vault import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__)
@@ -7,27 +8,28 @@ __all__ = ['Vault']
class Vault(BaseVault):
type = VaultTypeChoices.local
def is_active(self):
return True, ''
def _get(self, instance):
secret = getattr(instance, '_secret', None)
def _get(self, entry):
secret = getattr(entry.instance, '_secret', None)
return secret
def _create(self, instance):
def _create(self, entry):
""" Ignore """
pass
def _update(self, instance):
def _update(self, entry):
""" Ignore """
pass
def _delete(self, instance):
def _delete(self, entry):
""" Ignore """
pass
def _save_metadata(self, instance, metadata):
def _save_metadata(self, entry, metadata):
""" Ignore """
pass

View File

View File

@@ -0,0 +1,32 @@
from common.utils import get_logger
logger = get_logger(__name__)
class GeneralVaultMixin(object):
client = None
def is_active(self):
return self.client.is_active()
def _get(self, entry):
secret = self.client.get(name=entry.full_path)
return entry.get_decrypt_secret(secret)
def _create(self, entry):
secret = entry.get_encrypt_secret()
self.client.create(name=entry.full_path, secret=secret)
def _update(self, entry):
secret = entry.get_encrypt_secret()
self.client.update(name=entry.full_path, secret=secret)
def _delete(self, entry):
self.client.delete(name=entry.full_path)
def _save_metadata(self, entry, metadata):
try:
self.client.update_metadata(name=entry.full_path, metadata=metadata)
except Exception as e:
logger.error(f'save metadata error: {e}')

View File

@@ -49,9 +49,9 @@ class SecretStrategy(models.TextChoices):
class SSHKeyStrategy(models.TextChoices):
add = 'add', _('Append SSH KEY')
set = 'set', _('Empty and append SSH KEY')
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
set = 'set', _('Empty and append SSH KEY')
add = 'add', _('Append SSH KEY')
class TriggerChoice(models.TextChoices, TreeChoices):

View File

@@ -7,3 +7,5 @@ __all__ = ['VaultTypeChoices']
class VaultTypeChoices(models.TextChoices):
local = 'local', _('Database')
hcp = 'hcp', _('HCP Vault')
azure = 'azure', _('Azure Key Vault')
aws = 'aws', _('Amazon Secrets Manager')

View File

@@ -0,0 +1,8 @@
from common.exceptions import JMSException
from django.utils.translation import gettext_lazy as _
class VaultException(JMSException):
default_detail = _(
'Vault operation failed. Please retry or check your account information on Vault.'
)

View File

@@ -50,7 +50,7 @@ class Migration(migrations.Migration):
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')),
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
],
options={
'verbose_name': 'Change secret automation',
@@ -76,7 +76,7 @@ class Migration(migrations.Migration):
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')),
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('action', models.CharField(max_length=16, verbose_name='Action')),

View File

@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
on_delete=models.SET_NULL, verbose_name=_("Su from")
)
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
verbose_name=_("historical Account"))
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))

View File

@@ -14,13 +14,17 @@ from common.db import fields
from common.db.encoder import ModelJSONFieldEncoder
from common.utils import get_logger, lazyproperty
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel, OrgManager
__all__ = ['AccountBackupAutomation', 'AccountBackupExecution']
logger = get_logger(__file__)
class BaseBackupAutomationManager(OrgManager):
pass
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
types = models.JSONField(default=list)
backup_type = models.CharField(max_length=128, choices=AccountBackupType.choices,
@@ -47,6 +51,8 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
max_length=4096, blank=True, null=True, verbose_name=_('Zip encrypt password')
)
objects = BaseBackupAutomationManager.from_queryset(models.QuerySet)()
def __str__(self):
return f'{self.name}({self.org_id})'

View File

@@ -51,7 +51,7 @@ class AutomationExecution(AssetAutomationExecution):
class ChangeSecretMixin(SecretWithRandomMixin):
ssh_key_change_strategy = models.CharField(
choices=SSHKeyStrategy.choices, max_length=16,
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
default=SSHKeyStrategy.set_jms, verbose_name=_('SSH key change strategy')
)
get_all_assets: callable # get all assets

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, SecretType
from accounts.models import Account
from .base import AccountBaseAutomation
from .change_secret import ChangeSecretMixin
@@ -23,7 +23,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
create_usernames = set(usernames) - set(account_usernames)
create_account_objs = [
Account(
name=f'{username}-{secret_type}', username=username,
name=f"{username}-{secret_type}" if secret_type != SecretType.PASSWORD else username,
username=username,
secret_type=secret_type, asset=asset,
)
for username in create_usernames

View File

@@ -80,6 +80,7 @@ class VaultModelMixin(models.Model):
def mark_secret_save_to_vault(self):
self._secret = self._secret_save_to_vault_mark
self.skip_history_when_saving = True
self.save()
@property

View File

@@ -385,7 +385,7 @@ class AssetAccountBulkSerializer(
_results = {}
for asset in assets:
if asset not in secret_type_supports:
if asset not in secret_type_supports and asset.category != Category.CUSTOM:
_results[asset] = {
'error': _('Asset does not support this secret type: %s') % secret_type,
'state': 'error',

View File

@@ -10,7 +10,7 @@ from .base import BaseAccountSerializer
class PasswordRulesSerializer(serializers.Serializer):
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
length = serializers.IntegerField(min_value=8, max_value=36, default=16, label=_('Password length'))
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
digit = serializers.BooleanField(default=True, label=_('Digit'))

View File

@@ -63,6 +63,26 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
)},
}}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_ssh_key_change_strategy_choices()
def set_ssh_key_change_strategy_choices(self):
ssh_key_change_strategy = self.fields.get("ssh_key_change_strategy")
if not ssh_key_change_strategy:
return
ssh_key_change_strategy._choices.pop(SSHKeyStrategy.add, None)
def to_representation(self, instance):
data = super().to_representation(instance)
ssh_strategy_value = data.get('ssh_key_change_strategy', {}).get('value')
if ssh_strategy_value == SSHKeyStrategy.add:
data['ssh_key_change_strategy'] = {
'label': SSHKeyStrategy.set_jms.label,
'value': SSHKeyStrategy.set_jms.value
}
return data
@property
def model_type(self):
return AutomationTypes.change_secret
@@ -75,19 +95,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
if self.initial_data.get('secret_strategy') == SecretStrategy.custom:
return password_rules
length = password_rules.get('length')
try:
length = int(length)
except Exception as e:
logger.error(e)
msg = _("* Please enter the correct password length")
raise serializers.ValidationError(msg)
if length < 6 or length > 30:
msg = _('* Password length range 6-30 bits')
raise serializers.ValidationError(msg)
return password_rules
def validate(self, attrs):

View File

@@ -3,14 +3,18 @@ from collections import defaultdict
from django.db.models.signals import post_delete
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.functional import LazyObject
from django.utils.translation import gettext_noop
from accounts.backends import vault_client
from accounts.backends import vault_client, refresh_vault_client
from accounts.const import Source
from audits.const import ActivityChoices
from audits.signal_handlers import create_activities
from common.decorators import merge_delay_run
from common.signals import django_ready
from common.utils import get_logger, i18n_fmt
from common.utils.connection import RedisPubSub
from .exceptions import VaultException
from .models import Account, AccountTemplate
from .tasks.push_account import push_accounts_to_assets_task
@@ -19,6 +23,9 @@ logger = get_logger(__name__)
@receiver(pre_save, sender=Account)
def on_account_pre_save(sender, instance, **kwargs):
if getattr(instance, 'skip_history_when_saving', False):
return
if instance.version == 0:
instance.version = 1
else:
@@ -62,7 +69,7 @@ def create_accounts_activities(account, action='create'):
@receiver(post_save, sender=Account)
def on_account_create_by_template(sender, instance, created=False, **kwargs):
if not created or instance.source != Source.TEMPLATE:
if not created:
return
push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create')
@@ -78,16 +85,39 @@ class VaultSignalHandler(object):
@staticmethod
def save_to_vault(sender, instance, created, **kwargs):
if created:
vault_client.create(instance)
else:
vault_client.update(instance)
try:
if created:
vault_client.create(instance)
else:
vault_client.update(instance)
except Exception as e:
logger.error('Vault save failed: {}'.format(e))
raise VaultException()
@staticmethod
def delete_to_vault(sender, instance, **kwargs):
vault_client.delete(instance)
try:
vault_client.delete(instance)
except Exception as e:
logger.error('Vault delete failed: {}'.format(e))
raise VaultException()
for model in (Account, AccountTemplate, Account.history.model):
post_save.connect(VaultSignalHandler.save_to_vault, sender=model)
post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model)
class VaultPubSub(LazyObject):
def _setup(self):
self._wrapped = RedisPubSub('refresh_vault')
vault_pub_sub = VaultPubSub()
@receiver(django_ready)
def subscribe_vault_change(sender, **kwargs):
logger.debug("Start subscribe vault change")
vault_pub_sub.subscribe(lambda name: refresh_vault_client())

View File

@@ -5,6 +5,7 @@ from celery import shared_task
from django.utils.translation import gettext_lazy as _
from accounts.backends import vault_client
from accounts.const import VaultTypeChoices
from accounts.models import Account, AccountTemplate
from common.utils import get_logger
from orgs.utils import tmp_to_root_org
@@ -39,6 +40,9 @@ def sync_secret_to_vault():
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
print('\033[35m>>> 当前 Vault 功能未开启, 不需要同步')
return
if VaultTypeChoices.local == vault_client.type:
print('\033[31m>>> 当前第三方 Vault 客户端初始化失败,数据存储在本地数据库')
return
failed, skipped, succeeded = 0, 0, 0
to_sync_models = [Account, AccountTemplate, Account.history.model]
@@ -48,7 +52,8 @@ def sync_secret_to_vault():
for model in to_sync_models:
instances += list(model.objects.all())
with ThreadPoolExecutor(max_workers=10) as executor:
max_workers = 1 if VaultTypeChoices.azure == vault_client.type else 10
with ThreadPoolExecutor(max_workers=max_workers) as executor:
tasks = [executor.submit(sync_instance, instance) for instance in instances]
for future in as_completed(tasks):

View File

@@ -9,3 +9,5 @@ class ActionChoices(models.TextChoices):
warning = 'warning', _('Warn')
notice = 'notice', _('Notify')
notify_and_warn = 'notify_and_warn', _('Notify and warn')
face_verify = 'face_verify', _('Face Verify')
face_online = 'face_online', _('Face Online')

View File

@@ -70,6 +70,13 @@ class ActionAclSerializer(serializers.Serializer):
return
if not settings.XPACK_LICENSE_IS_VALID:
field_action._choices.pop(ActionChoices.review, None)
if not (
settings.XPACK_LICENSE_IS_VALID and
settings.XPACK_LICENSE_EDITION_ULTIMATE and
settings.FACE_RECOGNITION_ENABLED
):
field_action._choices.pop(ActionChoices.face_verify, None)
field_action._choices.pop(ActionChoices.face_online, None)
for choice in self.Meta.action_choices_exclude:
field_action._choices.pop(choice, None)

View File

@@ -32,7 +32,9 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
class Meta(BaseSerializer.Meta):
model = CommandFilterACL
fields = BaseSerializer.Meta.fields + ['command_groups']
action_choices_exclude = [ActionChoices.notice]
action_choices_exclude = [ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online]
class CommandReviewSerializer(serializers.Serializer):

View File

@@ -4,6 +4,7 @@ from common.serializers import MethodSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserACLSerializer
from .rules import RuleSerializer
from ..const import ActionChoices
from ..models import LoginACL
__all__ = ["LoginACLSerializer"]
@@ -17,6 +18,7 @@ class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
class Meta(BaseUserACLSerializer.Meta):
model = LoginACL
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
action_choices_exclude = [ActionChoices.face_online, ActionChoices.face_verify]
def get_rules_serializer(self):
return RuleSerializer()

View File

@@ -123,6 +123,10 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
NodeFilterBackend, AttrRulesFilterBackend
]
def perform_destroy(self, instance):
instance.accounts.update(su_from_id=None)
instance.delete()
def get_queryset(self):
queryset = super().get_queryset()
if queryset.model is not Asset:

View File

@@ -1,10 +1,10 @@
from django.db.models import Count
from django.db.models import Subquery, OuterRef, Count, Value
from django.db.models.functions import Coalesce
from django_filters import rest_framework as filters
from rest_framework import generics
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.const import AllTypes
from assets.models import Platform, Node, Asset, PlatformProtocol
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
@@ -42,7 +42,10 @@ class AssetPlatformViewSet(JMSModelViewSet):
def get_queryset(self):
# 因为没有走分页逻辑,所以需要这里 prefetch
queryset = super().get_queryset().annotate(assets_amount=Count('assets')).prefetch_related(
asset_count_subquery = Asset.objects.filter(platform=OuterRef('pk')).values('platform').annotate(
count=Count('id')).values('count')
queryset = super().get_queryset().annotate(
assets_amount=Coalesce(Subquery(asset_count_subquery), Value(0))).prefetch_related(
'protocols', 'automation', 'labels', 'labels__label'
)
queryset = queryset.filter(type__in=AllTypes.get_types_values())

View File

@@ -115,7 +115,7 @@ class PingGatewayManager:
@staticmethod
def before_runner_start():
print(">>> 开始执行测试网关可连接性任务")
print(_(">>> Start executing the task to test gateway connectivity"))
def get_accounts(self, gateway):
account = gateway.select_account

View File

@@ -112,7 +112,7 @@ class BaseType(TextChoices):
@classmethod
def get_choices(cls):
if not settings.XPACK_ENABLED:
if not settings.XPACK_LICENSE_IS_VALID:
choices = [(tp.value, tp.label) for tp in cls.get_community_types()]
else:
choices = cls.choices

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
from copy import deepcopy
from django.conf import settings
from django.utils.functional import lazy
from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin
@@ -29,15 +30,15 @@ class AllTypes(ChoicesMixin):
@classmethod
def choices(cls):
return lazy(cls.get_choices, list)()
@classmethod
def get_choices(cls):
choices = []
for tp in cls.includes:
choices.extend(tp.get_choices())
return choices
@classmethod
def get_choices(cls):
return cls.choices()
@classmethod
def filter_choices(cls, category):
choices = dict(cls.category_types()).get(category, cls).get_choices()

View File

@@ -10,7 +10,11 @@ from assets.tasks import execute_asset_automation_task
from common.const.choices import Trigger
from common.db.fields import EncryptJsonDictTextField
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel, OrgManager
class BaseAutomationManager(OrgManager):
pass
class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
@@ -21,6 +25,8 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
params = models.JSONField(default=dict, verbose_name=_("Parameters"))
objects = BaseAutomationManager.from_queryset(models.QuerySet)()
def __str__(self):
return self.name + '@' + str(self.created_by)

View File

@@ -18,7 +18,7 @@ from common.serializers.fields import LabeledChoiceField
from labels.models import Label
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes
from ...models import Asset, Node, Platform, Protocol
from ...models import Asset, Node, Platform, Protocol, Host, Device, Database, Cloud, Web, Custom
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
@@ -309,6 +309,17 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
})
return protocols_data_map.values()
def validate_platform(self, platform_data):
check_models = {Host, Device, Database, Cloud, Web, Custom}
if self.Meta.model not in check_models:
return platform_data
model_name = self.Meta.model.__name__.lower()
if model_name != platform_data.category:
raise serializers.ValidationError({
'platform': f"Platform does not match: {platform_data.name}"
})
return platform_data
@staticmethod
def update_account_su_from(accounts, include_su_from_accounts):
if not include_su_from_accounts:

View File

@@ -27,7 +27,7 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
model = Domain
fields_mini = ['id', 'name']
fields_small = fields_mini + ['comment']
fields_m2m = ['assets', 'gateways', 'assets_amount']
fields_m2m = ['assets', 'gateways', 'labels', 'assets_amount']
read_only_fields = ['date_created']
fields = fields_small + fields_m2m + read_only_fields
extra_kwargs = {

View File

@@ -7,6 +7,7 @@ from django.db.models import F, Value, CharField, Q
from django.db.models.functions import Cast
from django.http import HttpResponse, FileResponse
from django.utils.encoding import escape_uri_path
from django_celery_beat.models import PeriodicTask
from rest_framework import generics
from rest_framework import status
from rest_framework import viewsets
@@ -22,6 +23,9 @@ from common.plugins.es import QuerySet as ESQuerySet
from common.sessions.cache import user_session_manager
from common.storage.ftp_file import FTPFileStorageHandler
from common.utils import is_uuid, get_logger, lazyproperty
from ops.const import Types
from ops.models import Job
from ops.serializers.job import JobSerializer
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
from orgs.models import Organization
from orgs.utils import current_org, tmp_to_root_org
@@ -39,14 +43,14 @@ from .serializers import (
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
OperateLogSerializer, OperateLogActionDetailSerializer,
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
FileSerializer, UserSessionSerializer
FileSerializer, UserSessionSerializer, JobsAuditSerializer
)
from .utils import construct_userlogin_usernames
logger = get_logger(__name__)
class JobAuditViewSet(OrgReadonlyModelViewSet):
class JobLogAuditViewSet(OrgReadonlyModelViewSet):
model = JobLog
extra_filter_backends = [DatetimeRangeFilterBackend]
date_range_filter_fields = [
@@ -58,6 +62,35 @@ class JobAuditViewSet(OrgReadonlyModelViewSet):
ordering = ['-date_start']
class JobsAuditViewSet(OrgModelViewSet):
model = Job
search_fields = ['creator__name']
filterset_fields = ['creator__name']
serializer_class = JobsAuditSerializer
ordering = ['-is_periodic', '-date_updated']
http_method_names = ['get', 'options', 'patch']
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.exclude(type=Types.upload_file).filter(instant=False)
return queryset
def perform_update(self, serializer):
job = self.get_object()
is_periodic = serializer.validated_data.get('is_periodic')
if job.is_periodic != is_periodic:
job.is_periodic = is_periodic
job.save()
name, task, args, kwargs = job.get_register_task()
task_obj = PeriodicTask.objects.filter(name=name).first()
if task_obj:
is_periodic = job.is_periodic
if task_obj.enabled != is_periodic:
task_obj.enabled = is_periodic
task_obj.save()
return super().perform_update(serializer)
class FTPLogViewSet(OrgModelViewSet):
model = FTPLog
serializer_class = FTPLogSerializer
@@ -146,7 +179,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
username = self.request.user.username
q = Q(username=username) | Q(username__icontains=f'({username})')
qs = qs.filter(q)
return qs
@@ -187,9 +222,13 @@ class ResourceActivityAPIView(generics.ListAPIView):
'id', 'datetime', 'r_detail', 'r_detail_id',
'r_user', 'r_action', 'r_type'
)
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
org_q = Q()
if not current_org.is_root():
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
with tmp_to_root_org():
qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id)
qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id)

View File

@@ -74,6 +74,9 @@ class OperateLogStore(object):
@classmethod
def convert_diff_friendly(cls, op_log):
diff_list = list()
# 标记翻译字符串
labels = _("labels")
operate_log_id = _("operate_log_id")
handler = cls._get_special_handler(op_log.resource_type)
for k, v in op_log.diff.items():
before_value, after_value = cls.split_value(v)

View File

@@ -7,7 +7,7 @@ from audits.backends.db import OperateLogStore
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import reverse, i18n_trans
from common.utils.timezone import as_current_tz
from ops.serializers.job import JobExecutionSerializer
from ops.serializers.job import JobExecutionSerializer, JobSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from terminal.models import Session
from users.models import User
@@ -34,6 +34,30 @@ class JobLogSerializer(JobExecutionSerializer):
}
class JobsAuditSerializer(JobSerializer):
material = serializers.ReadOnlyField(label=_("Command"))
summary = serializers.ReadOnlyField(label=_("Summary"))
crontab = serializers.ReadOnlyField(label=_("Execution cycle"))
is_periodic_display = serializers.BooleanField(read_only=True, source='is_periodic')
class Meta(JobSerializer.Meta):
read_only_fields = [
"id", 'name', 'args', 'material', 'type', 'crontab', 'interval', 'date_last_run', 'summary', 'created_by',
'is_periodic_display'
]
fields = read_only_fields + ['is_periodic']
def validate(self, attrs):
allowed_fields = {'is_periodic'}
submitted_fields = set(attrs.keys())
invalid_fields = submitted_fields - allowed_fields
if invalid_fields:
raise serializers.ValidationError(
f"Updating {', '.join(invalid_fields)} fields is not allowed"
)
return attrs
class FTPLogSerializer(serializers.ModelSerializer):
operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate"))

View File

@@ -14,7 +14,7 @@ from audits.handler import (
create_or_update_operate_log, get_instance_dict_from_cache
)
from audits.utils import model_to_dict_for_operate_log as model_to_dict
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, OP_LOG_SKIP_SIGNAL
from common.signals import django_ready
from jumpserver.utils import current_request
from ..const import MODELS_NEED_RECORD, ActionChoices
@@ -77,7 +77,7 @@ def signal_of_operate_log_whether_continue(
condition = True
if not instance:
condition = False
if instance and getattr(instance, SKIP_SIGNAL, False):
if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False):
condition = False
# 不记录组件的操作日志
user = current_request.user if current_request else None
@@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord'
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable'
}
include_models = {'UserSession'}
for i, app in enumerate(apps.get_models(), 1):

View File

@@ -13,7 +13,9 @@ router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log')
router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log')
router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
router.register(r'job-logs', api.JobAuditViewSet, 'job-log')
router.register(r'job-logs', api.JobLogAuditViewSet, 'job-log')
router.register(r'jobs', api.JobsAuditViewSet, 'job')
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')

View File

@@ -15,3 +15,4 @@ from .ssh_key import *
from .sso import *
from .temp_token import *
from .token import *
from .face import *

View File

@@ -24,11 +24,13 @@ from common.utils.http import is_true, is_false
from orgs.mixins.api import RootOrgViewMixin
from orgs.utils import tmp_to_org
from perms.models import ActionChoices
from terminal.connect_methods import NativeClient, ConnectMethodUtil
from terminal.connect_methods import NativeClient, ConnectMethodUtil, WebMethod
from terminal.models import EndpointRule, Endpoint
from users.const import FileNameConflictResolution
from users.const import RDPSmartSize, RDPColorQuality
from users.models import Preference
from .face import FaceMonitorContext
from ..mixins import AuthFaceMixin
from ..models import ConnectionToken, date_expired_default
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@@ -67,6 +69,36 @@ class RDPFileClientProtocolURLMixin:
'bookmarktype:i': '3',
'use redirection server name:i': '0',
}
# copy from
# https://learn.microsoft.com/zh-cn/windows-server/administration/performance-tuning/role/remote-desktop/session-hosts
rdp_low_speed_broadband_option = {
"connection type:i": 2,
"disable wallpaper:i": 1,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1,
"disable menu anims:i": 1,
"allow font smoothing:i": 0,
"allow desktop composition:i": 0,
"disable themes:i": 0
}
rdp_high_speed_broadband_option = {
"connection type:i": 4,
"disable wallpaper:i": 0,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1,
"disable menu anims:i": 0,
"allow font smoothing:i": 0,
"allow desktop composition:i": 1,
"disable themes:i": 0
}
RDP_CONNECTION_SPEED_OPTION_MAP = {
"auto": {},
"low_speed_broadband": rdp_low_speed_broadband_option,
"high_speed_broadband": rdp_high_speed_broadband_option,
}
# 设置多屏显示
multi_mon = is_true(self.request.query_params.get('multi_mon'))
if multi_mon:
@@ -91,13 +123,15 @@ class RDPFileClientProtocolURLMixin:
# rdp_options['domain:s'] = token.account_ad_domain
# 设置宽高
height = self.request.query_params.get('height')
width = self.request.query_params.get('width')
if width and height:
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
rdp_options['dynamic resolution:i'] = '0'
resolution_value = token.connect_options.get('resolution', 'auto')
if resolution_value != 'auto':
width, height = resolution_value.split('x')
if width and height:
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
rdp_options['dynamic resolution:i'] = '0'
color_quality = self.request.query_params.get('rdp_color_quality')
color_quality = color_quality if color_quality else os.getenv('JUMPSERVER_COLOR_DEPTH', RDPColorQuality.HIGH)
@@ -115,6 +149,8 @@ class RDPFileClientProtocolURLMixin:
rdp = token.asset.platform.protocols.filter(name='rdp').first()
if rdp and rdp.setting.get('console'):
rdp_options['administrative session:i'] = '1'
rdp_connection_speed = token.connect_options.get('rdp_connection_speed', 'auto')
rdp_options.update(RDP_CONNECTION_SPEED_OPTION_MAP.get(rdp_connection_speed, {}))
# 文件名
name = token.asset.name
@@ -221,6 +257,8 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: callable
perform_create: callable
validate_exchange_token: callable
need_face_verify: bool
create_face_verify: callable
@action(methods=['POST', 'GET'], detail=True, url_path='rdp-file')
def get_rdp_file(self, request, *args, **kwargs):
@@ -280,10 +318,13 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.date_expired = date_expired_default()
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data, status=status.HTTP_201_CREATED)
response = Response(serializer.data, status=status.HTTP_201_CREATED)
if self.need_face_verify:
self.create_face_verify(response)
return response
class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet):
class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
'user_display', 'asset_display'
)
@@ -304,6 +345,8 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
input_username = ''
need_face_verify = False
face_monitor_token = ''
def get_queryset(self):
queryset = ConnectionToken.objects \
@@ -355,8 +398,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
asset = data.get('asset')
account_name = data.get('account')
protocol = data.get('protocol')
connect_method = data.get('connect_method')
self.input_username = self.get_input_username(data)
_data = self._validate(user, asset, account_name, protocol)
_data = self._validate(user, asset, account_name, protocol, connect_method)
data.update(_data)
return serializer
@@ -364,12 +408,12 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
user = token.user
asset = token.asset
account_name = token.account
_data = self._validate(user, asset, account_name, token.protocol)
_data = self._validate(user, asset, account_name, token.protocol, token.connect_method)
for k, v in _data.items():
setattr(token, k, v)
return token
def _validate(self, user, asset, account_name, protocol):
def _validate(self, user, asset, account_name, protocol, connect_method):
data = dict()
data['org_id'] = asset.org_id
data['user'] = user
@@ -385,10 +429,16 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
if account.username != AliasAccount.INPUT:
data['input_username'] = ''
ticket = self._validate_acl(user, asset, account)
ticket = self._validate_acl(user, asset, account, connect_method)
if ticket:
data['from_ticket'] = ticket
if ticket or self.need_face_verify:
data['is_active'] = False
if self.face_monitor_token:
FaceMonitorContext.get_or_create_context(self.face_monitor_token,
self.request.user.id)
data['face_monitor_token'] = self.face_monitor_token
return data
@staticmethod
@@ -417,7 +467,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
after=after, object_name=object_name
)
def _validate_acl(self, user, asset, account):
def _validate_acl(self, user, asset, account, connect_method):
from acls.models import LoginAssetACL
kwargs = {'user': user, 'asset': asset, 'account': account}
if account.username == AliasAccount.INPUT:
@@ -444,6 +494,26 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
assignees=acl.reviewers.all(), org_id=asset.org_id
)
return ticket
if acl.is_action(acl.ActionChoices.face_verify):
if not self.request.query_params.get('face_verify'):
msg = _('ACL action is face verify')
raise JMSException(code='acl_face_verify', detail=msg)
self.need_face_verify = True
if acl.is_action(acl.ActionChoices.face_online):
if connect_method not in [WebMethod.web_cli, WebMethod.web_gui]:
msg = _('ACL action not supported for this asset')
raise JMSException(detail=msg, code='acl_face_online_not_supported')
face_verify = self.request.query_params.get('face_verify')
face_monitor_token = self.request.query_params.get('face_monitor_token')
if not face_verify or not face_monitor_token:
msg = _('ACL action is face online')
raise JMSException(code='acl_face_online', detail=msg)
self.need_face_verify = True
self.face_monitor_token = face_monitor_token
if acl.is_action(acl.ActionChoices.notice):
reviewers = acl.reviewers.all()
if not reviewers:
@@ -455,9 +525,22 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
reviewer, asset, user, account, self.input_username
).publish_async()
def create_face_verify(self, response):
if not self.request.user.face_vector:
raise JMSException(code='no_face_feature', detail=_('No available face feature'))
connection_token_id = response.data.get('id')
context_data = {
"action": "login_asset",
"connection_token_id": connection_token_id,
}
face_verify_token = self.create_face_verify_context(context_data)
response.data['face_token'] = face_verify_token
def create(self, request, *args, **kwargs):
try:
response = super().create(request, *args, **kwargs)
if self.need_face_verify:
self.create_face_verify(response)
except JMSException as e:
data = {'code': e.detail.code, 'detail': e.detail}
return Response(data, status=e.status_code)
@@ -472,6 +555,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'check': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -484,6 +568,28 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
def get_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['GET'], detail=True, url_path='check')
def check(self, request, *args, **kwargs):
instance = self.get_object()
data = {
"detail": "OK",
"code": "perm_ok",
"expired": instance.is_expired
}
try:
self._validate_perm(
instance.user,
instance.asset,
instance.account,
instance.protocol
)
except JMSException as e:
data['code'] = e.detail.code
data['detail'] = str(e.detail)
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return Response(data=data, status=status.HTTP_200_OK)
@action(methods=['PATCH'], detail=False)
def renewal(self, request, *args, **kwargs):
from common.utils.timezone import as_current_tz

View File

@@ -0,0 +1,256 @@
from django.core.cache import cache
from django.utils.translation import gettext as _
from rest_framework.generics import CreateAPIView, RetrieveAPIView
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.permissions import AllowAny
from rest_framework.exceptions import NotFound
from common.permissions import IsServiceAccount
from common.utils import get_logger, get_object_or_none
from orgs.utils import tmp_to_root_org
from terminal.api.session.task import create_sessions_tasks
from users.models import User
from .. import serializers
from ..mixins import AuthMixin
from ..const import FACE_CONTEXT_CACHE_KEY_PREFIX, FACE_SESSION_KEY, FACE_CONTEXT_CACHE_TTL, FaceMonitorActionChoices
from ..models import ConnectionToken
from ..serializers.face import FaceMonitorCallbackSerializer, FaceMonitorContextSerializer
logger = get_logger(__name__)
__all__ = [
'FaceCallbackApi',
'FaceContextApi',
'FaceMonitorContext',
'FaceMonitorContextApi',
'FaceMonitorCallbackApi'
]
class FaceCallbackApi(AuthMixin, CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = serializers.FaceCallbackSerializer
def perform_create(self, serializer):
token = serializer.validated_data.get('token')
context = self._get_context_from_cache(token)
if not serializer.validated_data.get('success', False):
self._update_context_with_error(
context,
serializer.validated_data.get('error_message', 'Unknown error')
)
return Response(status=200)
face_code = serializer.validated_data.get('face_code')
if not face_code:
self._update_context_with_error(context, "missing field 'face_code'")
raise ValidationError({'error': "missing field 'face_code'"})
try:
self._handle_success(context, face_code)
except Exception as e:
self._update_context_with_error(context, str(e))
return Response(status=200)
@staticmethod
def get_face_cache_key(token):
return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def _get_context_from_cache(self, token):
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise ValidationError({'error': "token not exists or expired"})
return context
def _update_context_with_error(self, context, error_message):
context.update({
'is_finished': True,
'success': False,
'error_message': error_message,
})
self._update_cache(context)
def _update_cache(self, context):
cache_key = self.get_face_cache_key(context['token'])
cache.set(cache_key, context, FACE_CONTEXT_CACHE_TTL)
def _handle_success(self, context, face_code):
context.update({
'is_finished': True,
'success': True,
'face_code': face_code
})
action = context.get('action', None)
if action == 'login_asset':
user_id = context.get('user_id')
user = User.objects.get(id=user_id)
if user.check_face(face_code):
with tmp_to_root_org():
connection_token_id = context.get('connection_token_id')
token = ConnectionToken.objects.filter(id=connection_token_id).first()
token.is_active = True
token.save()
else:
context.update({
'success': False,
'error_message': _('Facial comparison failed')
})
self._update_cache(context)
class FaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView):
permission_classes = (AllowAny,)
face_token_session_key = FACE_SESSION_KEY
@staticmethod
def get_face_cache_key(token):
return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def new_face_context(self):
return self.create_face_verify_context()
def post(self, request, *args, **kwargs):
token = self.new_face_context()
return Response({'token': token})
def get(self, request, *args, **kwargs):
token = self.request.session.get(self.face_token_session_key)
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise NotFound({'error': "Token does not exist or has expired."})
return Response({
"is_finished": context.get('is_finished', False),
"success": context.get('success', False),
"error_message": _(context.get("error_message", ''))
})
class FaceMonitorContext:
def __init__(self, token, user_id, session_ids=None):
self.token = token
self.user_id = user_id
if session_ids is None:
self.session_ids = []
else:
self.session_ids = session_ids
@classmethod
def get_cache_key(cls, token):
return 'FACE_MONITOR_CONTEXT_{}'.format(token)
@classmethod
def get_or_create_context(cls, token, user_id):
context = cls.get(token)
if not context:
context = FaceMonitorContext(token=token,
user_id=user_id)
context.save()
return context
def add_session(self, session_id):
self.session_ids.append(session_id)
self.save()
@classmethod
def get(cls, token):
cache_key = cls.get_cache_key(token)
return cache.get(cache_key, None)
def save(self):
cache_key = self.get_cache_key(self.token)
cache.set(cache_key, self)
def close(self):
self.terminal_sessions()
self._destroy()
def _destroy(self):
cache_key = self.get_cache_key(self.token)
cache.delete(cache_key)
def pause_sessions(self):
self._send_task('lock_session')
def resume_sessions(self):
self._send_task('unlock_session')
def terminal_sessions(self):
self._send_task("kill_session")
def _send_task(self, task_name):
create_sessions_tasks(self.session_ids, 'facelive', task_name=task_name)
class FaceMonitorContextApi(CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = FaceMonitorContextSerializer
def perform_create(self, serializer):
face_monitor_token = serializer.validated_data.get('face_monitor_token')
session_id = serializer.validated_data.get('session_id')
context = FaceMonitorContext.get(face_monitor_token)
context.add_session(session_id)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(status=201)
class FaceMonitorCallbackApi(CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = FaceMonitorCallbackSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.validated_data.get('token')
context = FaceMonitorContext.get(token=token)
is_finished = serializer.validated_data.get('is_finished')
if is_finished:
context.close()
return Response(status=200)
action = serializer.validated_data.get('action')
if action == FaceMonitorActionChoices.Verify:
user = get_object_or_none(User, pk=context.user_id)
face_codes = serializer.validated_data.get('face_codes')
if not user:
context.save()
return Response(data={'msg': 'user {} not found'
.format(context.user_id)}, status=400)
if not face_codes or not self._check_face_codes(face_codes, user):
context.save()
return Response(data={'msg': 'face codes not matched'}, status=400)
if action == FaceMonitorActionChoices.Pause:
context.pause_sessions()
if action == FaceMonitorActionChoices.Resume:
context.resume_sessions()
context.save()
return Response(status=200)
@staticmethod
def _check_face_codes(face_codes, user):
matched = False
for face_code in face_codes:
matched = user.check_face(face_code,
distance_threshold=0.45,
similarity_threshold=0.92)
if matched:
break
return matched

View File

@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
#
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from rest_framework import exceptions
from rest_framework.generics import CreateAPIView
from rest_framework.generics import CreateAPIView, RetrieveAPIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
@@ -20,10 +19,12 @@ from ..mixins import AuthMixin
logger = get_logger(__name__)
__all__ = [
'MFAChallengeVerifyApi', 'MFASendCodeApi'
'MFAChallengeVerifyApi', 'MFASendCodeApi',
]
# MFASelectAPi 原来的名字
class MFASendCodeApi(AuthMixin, CreateAPIView):
"""

View File

@@ -1,5 +1,6 @@
import time
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponseRedirect
from django.shortcuts import reverse
@@ -40,12 +41,15 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
return user, None
@staticmethod
def safe_send_code(token, code, target, form_type, content):
def safe_send_code(token, code, target, form_type, content, user_info):
token_sent_key = '{}_send_at'.format(token)
token_send_at = cache.get(token_sent_key, 0)
if token_send_at:
raise IntervalTooShort(60)
SendAndVerifyCodeUtil(target, code, backend=form_type, **content).gen_and_send_async()
tooler = SendAndVerifyCodeUtil(
target, code, backend=form_type, user_info=user_info, **content
)
tooler.gen_and_send_async()
cache.set(token_sent_key, int(time.time()), 60)
def prepare_code_data(self, user_info, serializer):
@@ -61,7 +65,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
if not user:
raise ValueError(err)
code = random_string(6, lower=False, upper=False)
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
context = {
'user': user, 'title': subject, 'code': code,
@@ -82,7 +86,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
code, target, form_type, content = self.prepare_code_data(user_info, serializer)
except ValueError as e:
return Response({'error': str(e)}, status=400)
self.safe_send_code(token, code, target, form_type, content)
self.safe_send_code(token, code, target, form_type, content, user_info)
return Response({'data': 'ok'}, status=200)

View File

@@ -23,10 +23,9 @@ class JMSBaseAuthBackend:
Reject users with is_valid=False. Custom user models that don't have
that attribute are allowed.
"""
# 在 check_user_auth 中进行了校验,可以返回对应的错误信息
# is_valid = getattr(user, 'is_valid', None)
# return is_valid or is_valid is None
return True
# 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
# allow user to authenticate
def username_allow_authenticate(self, username):
@@ -52,6 +51,14 @@ class JMSBaseAuthBackend:
logger.info(info)
return allow
def get_user(self, user_id):
""" 三方用户认证成功后 request.user 赋值时会调用 backend 的当前方法获取用户 """
try:
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
pass

View File

@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
import django_cas_ng.views
from django.urls import path
from .views import CASLoginView
from .views import CASLoginView, CASCallbackClientView
urlpatterns = [
path('login/', CASLoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
path('login/client', CASCallbackClientView.as_view(), name='cas-proxy-callback-client'),
]

View File

@@ -1,9 +1,12 @@
from django_cas_ng.views import LoginView
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.views.generic import View
from django_cas_ng.views import LoginView
__all__ = ['LoginView']
from authentication.views.utils import redirect_to_guard_view
class CASLoginView(LoginView):
def get(self, request):
@@ -13,3 +16,8 @@ class CASLoginView(LoginView):
return HttpResponseRedirect('/')
class CASCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')

View File

@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from authentication.signals import user_auth_failed, user_auth_success
from common.utils import get_logger
from .base import JMSModelBackend
from .base import JMSBaseAuthBackend
logger = get_logger(__file__)
@@ -20,9 +20,10 @@ if settings.AUTH_CUSTOM:
logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e))
class CustomAuthBackend(JMSModelBackend):
class CustomAuthBackend(JMSBaseAuthBackend):
def is_enabled(self):
@staticmethod
def is_enabled():
return settings.AUTH_CUSTOM and callable(custom_authenticate_method)
@staticmethod
@@ -35,10 +36,10 @@ class CustomAuthBackend(JMSModelBackend):
)
return user, created
def authenticate(self, request, username=None, password=None, **kwargs):
def authenticate(self, request, username=None, password=None):
try:
userinfo: dict = custom_authenticate_method(
username=username, password=password, **kwargs
username=username, password=password
)
user, created = self.get_or_create_user_from_userinfo(userinfo)
except Exception as e:

View File

@@ -3,8 +3,9 @@
import abc
import ldap
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, valid_cache_key
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
from users.utils import construct_user_email
@@ -146,30 +147,53 @@ class LDAPHAAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
class LDAPUser(_LDAPUser):
def __init__(self, backend, username=None, user=None, request=None):
super().__init__(backend=backend, username=username, user=user, request=request)
config_prefix = "" if isinstance(self.backend, LDAPAuthorizationBackend) else "_ha"
self.user_dn_cache_key = valid_cache_key(
f"django_auth_ldap{config_prefix}.user_dn.{self._username}"
)
self.category = f"ldap{config_prefix}"
self.search_filter = getattr(settings, f"AUTH_LDAP{config_prefix.upper()}_SEARCH_FILTER", None)
self.search_ou = getattr(settings, f"AUTH_LDAP{config_prefix.upper()}_SEARCH_OU", None)
def _search_for_user_dn_from_ldap_util(self):
from settings.utils import LDAPServerUtil
util = LDAPServerUtil()
util = LDAPServerUtil(category=self.category)
user_dn = util.search_for_user_dn(self._username)
return user_dn
def _load_user_dn(self):
"""
Populates self._user_dn with the distinguished name of our user.
This will either construct the DN from a template in
AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it.
If we have to search, we'll cache the DN.
"""
if self._using_simple_bind_mode():
self._user_dn = self._construct_simple_user_dn()
else:
if self.settings.CACHE_TIMEOUT > 0:
self._user_dn = cache.get_or_set(
self.user_dn_cache_key, self._search_for_user_dn, self.settings.CACHE_TIMEOUT
)
else:
self._user_dn = self._search_for_user_dn()
def _search_for_user_dn(self):
"""
This method was overridden because the AUTH_LDAP_USER_SEARCH
configuration in the settings.py file
is configured with a `lambda` problem value
"""
if isinstance(self.backend, LDAPAuthorizationBackend):
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_SEARCH_OU
else:
search_filter = settings.AUTH_LDAP_HA_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_HA_SEARCH_OU
user_search_union = [
LDAPSearch(
USER_SEARCH, ldap.SCOPE_SUBTREE,
search_filter
self.search_filter
)
for USER_SEARCH in str(search_ou).split("|")
for USER_SEARCH in str(self.search_ou).split("|")
]
search = LDAPSearchUnion(*user_search_union)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import base64
import requests
from django.utils.translation import gettext_lazy as _
@@ -17,7 +18,7 @@ from common.exceptions import JMSException
from .signals import (
oauth2_create_or_update_user
)
from ..base import JMSModelBackend
from ..base import JMSBaseAuthBackend
__all__ = ['OAuth2Backend']
@@ -25,7 +26,7 @@ __all__ = ['OAuth2Backend']
logger = get_logger(__name__)
class OAuth2Backend(JMSModelBackend):
class OAuth2Backend(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_OAUTH2
@@ -67,15 +68,7 @@ class OAuth2Backend(JMSModelBackend):
response_data = response_data['data']
return response_data
@staticmethod
def get_query_dict(response_data, query_dict):
query_dict.update({
'uid': response_data.get('uid', ''),
'access_token': response_data.get('access_token', '')
})
return query_dict
def authenticate(self, request, code=None, **kwargs):
def authenticate(self, request, code=None):
log_prompt = "Process authenticate [OAuth2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if code is None:
@@ -83,29 +76,31 @@ class OAuth2Backend(JMSModelBackend):
return None
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'grant_type': 'authorization_code', 'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT:
separator = '&'
else:
separator = '?'
separator = '&' if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT else '?'
access_token_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, separator=separator, query=urlencode(query_dict)
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT,
separator=separator, query=urlencode(query_dict)
)
# token_method -> get, post(post_data), post_json
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
encoded_credentials = base64.b64encode(
f"{settings.AUTH_OAUTH2_CLIENT_ID}:{settings.AUTH_OAUTH2_CLIENT_SECRET}".encode()
).decode()
headers = {
'Accept': 'application/json'
'Accept': 'application/json', 'Authorization': f'Basic {encoded_credentials}'
}
if token_method.startswith('post'):
body_key = 'json' if token_method.endswith('json') else 'data'
query_dict.update({
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
})
access_token_response = requests.post(
access_token_url, headers=headers, **{body_key: query_dict}
)
@@ -121,22 +116,12 @@ class OAuth2Backend(JMSModelBackend):
logger.error(log_prompt.format(error))
return None
query_dict = self.get_query_dict(response_data, query_dict)
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
if '?' in settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT:
separator = '&'
else:
separator = '?'
userinfo_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT, separator=separator,
query=urlencode(query_dict)
)
userinfo_url = settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
userinfo_response = requests.get(userinfo_url, headers=headers)
try:
userinfo_response.raise_for_status()

View File

@@ -4,9 +4,9 @@ from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
path('callback/client/', views.OAuth2AuthCallbackClientView.as_view(), name='login-callback-client'),
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
]

View File

@@ -1,16 +1,16 @@
from django.views import View
from django.conf import settings
from django.contrib import auth
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
from django.views import View
from authentication.mixins import authenticate
from authentication.utils import build_absolute_uri
from authentication.views.mixins import FlashMessageMixin
from authentication.mixins import authenticate
from authentication.views.utils import redirect_to_guard_view
from common.utils import get_logger
logger = get_logger(__file__)
@@ -67,6 +67,13 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(redirect_url)
class OAuth2AuthCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')
class OAuth2EndSessionView(View):
http_method_names = ['get', 'post', ]

View File

@@ -13,10 +13,8 @@ import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.urls import reverse
from rest_framework.exceptions import ParseError
from authentication.signals import user_auth_success, user_auth_failed
from authentication.utils import build_absolute_uri_for_oidc
@@ -88,7 +86,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
"""
@ssl_verification
def authenticate(self, request, nonce=None, code_verifier=None, **kwargs):
def authenticate(self, request, nonce=None, code_verifier=None):
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
logger.debug(log_prompt.format('start'))
@@ -107,7 +105,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# parameters because we won't be able to get a valid token for the user in that case.
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
logger.debug(log_prompt.format('Authorization code or state value is missing'))
raise SuspiciousOperation('Authorization code or state value is missing')
return
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
@@ -165,7 +163,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
@@ -206,7 +204,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)
@@ -235,15 +233,15 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
class OIDCAuthPasswordBackend(OIDCBaseBackend):
@ssl_verification
def authenticate(self, request, username=None, password=None, **kwargs):
def authenticate(self, request, username=None, password=None):
try:
return self._authenticate(request, username, password, **kwargs)
return self._authenticate(request, username, password)
except Exception as e:
error = f'Authenticate exception: {e}'
logger.error(error, exc_info=True)
return
def _authenticate(self, request, username=None, password=None, **kwargs):
def _authenticate(self, request, username=None, password=None):
"""
https://oauth.net/2/
https://aaronparecki.com/oauth-2-simplified/#password

View File

@@ -4,7 +4,9 @@
import warnings
import contextlib
import requests
import inspect
from functools import wraps
from django.conf import settings
from urllib3.exceptions import InsecureRequestWarning
@@ -52,6 +54,7 @@ def no_ssl_verification():
def ssl_verification(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
return func(*args, **kwargs)

View File

@@ -12,9 +12,9 @@ from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
path('callback/client/', views.OIDCAuthCallbackClientView.as_view(), name='login-callback-client'),
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
]

View File

@@ -22,13 +22,14 @@ from django.http import HttpResponseRedirect, QueryDict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.http import urlencode
from django.views.generic import View
from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from authentication.utils import build_absolute_uri_for_oidc
from authentication.views.mixins import FlashMessageMixin
from common.utils import safe_next_url
from .utils import get_logger
from ...views.utils import redirect_to_guard_view
logger = get_logger(__file__)
@@ -208,6 +209,13 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
class OIDCAuthCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')
class OIDCEndSessionView(View):
""" Allows to end the session of any user authenticated using OpenID Connect.

View File

@@ -13,7 +13,7 @@ class Passkey(JMSBaseModel):
added_on = models.DateTimeField(auto_now_add=True, verbose_name=_("Added on"))
date_last_used = models.DateTimeField(null=True, default=None, verbose_name=_("Date last used"))
credential_id = models.CharField(max_length=255, unique=True, null=False, verbose_name=_("Credential ID"))
token = models.CharField(max_length=255, null=False, verbose_name=_("Token"))
token = models.CharField(max_length=1024, null=False, verbose_name=_("Token"))
def __str__(self):
return self.name

View File

@@ -51,10 +51,10 @@ class RadiusBaseBackend(CreateUserMixin, JMSBaseAuthBackend):
class RadiusBackend(RadiusBaseBackend, RADIUSBackend):
def authenticate(self, request, username='', password='', **kwargs):
def authenticate(self, request, username='', password=''):
return super().authenticate(request, username=username, password=password)
class RadiusRealmBackend(RadiusBaseBackend, RADIUSRealmBackend):
def authenticate(self, request, username='', password='', realm=None, **kwargs):
def authenticate(self, request, username='', password='', realm=None):
return super().authenticate(request, username=username, password=password, realm=realm)

View File

@@ -10,14 +10,14 @@ from .signals import (
saml2_create_or_update_user
)
from authentication.signals import user_auth_failed, user_auth_success
from ..base import JMSModelBackend
from ..base import JMSBaseAuthBackend
__all__ = ['SAML2Backend']
logger = get_logger(__name__)
class SAML2Backend(JMSModelBackend):
class SAML2Backend(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SAML2
@@ -42,7 +42,7 @@ class SAML2Backend(JMSModelBackend):
)
return user, created
def authenticate(self, request, saml_user_data=None, **kwargs):
def authenticate(self, request, saml_user_data=None):
log_prompt = "Process authenticate [SAML2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if saml_user_data is None:

View File

@@ -4,10 +4,10 @@ from django.urls import path
from . import views
urlpatterns = [
path('login/', views.Saml2AuthRequestView.as_view(), name='saml2-login'),
path('logout/', views.Saml2EndSessionView.as_view(), name='saml2-logout'),
path('callback/', views.Saml2AuthCallbackView.as_view(), name='saml2-callback'),
path('callback/client/', views.Saml2AuthCallbackClientView.as_view(), name='saml2-callback-client'),
path('metadata/', views.Saml2AuthMetadataView.as_view(), name='saml2-metadata'),
]

View File

@@ -19,6 +19,7 @@ from onelogin.saml2.idp_metadata_parser import (
from authentication.views.mixins import FlashMessageMixin
from common.utils import get_logger
from .settings import JmsSaml2Settings
from ...views.utils import redirect_to_guard_view
logger = get_logger(__file__)
@@ -298,6 +299,13 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
return super().dispatch(*args, **kwargs)
class Saml2AuthCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')
class Saml2AuthMetadataView(View, PrepareRequestMixin):
def get(self, request):

View File

@@ -1,57 +1,41 @@
from django.conf import settings
from .base import JMSModelBackend
from .base import JMSBaseAuthBackend
class SSOAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class SSOAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SSO
def authenticate(self, request, sso_token=None, **kwargs):
def authenticate(self):
pass
class WeComAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class WeComAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_WECOM
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
class DingTalkAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class DingTalkAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_DINGTALK
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
class FeiShuAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class FeiShuAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_FEISHU
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
@@ -61,23 +45,15 @@ class LarkAuthentication(FeiShuAuthentication):
return settings.AUTH_LARK
class SlackAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class SlackAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SLACK
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
class AuthorizationTokenAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
class AuthorizationTokenAuthentication(JMSBaseAuthBackend):
def authenticate(self):
pass

View File

@@ -3,13 +3,17 @@ from django.conf import settings
from django.core.exceptions import PermissionDenied
from authentication.models import TempToken
from .base import JMSModelBackend
from .base import JMSBaseAuthBackend
class TempTokenAuthBackend(JMSModelBackend):
class TempTokenAuthBackend(JMSBaseAuthBackend):
model = TempToken
def authenticate(self, request, username='', password='', *args, **kwargs):
@staticmethod
def is_enabled():
return settings.AUTH_TEMP_TOKEN
def authenticate(self, request, username='', password=''):
token = self.model.objects.filter(username=username, secret=password).first()
if not token:
return None
@@ -21,6 +25,3 @@ class TempTokenAuthBackend(JMSModelBackend):
token.save()
return token.user
@staticmethod
def is_enabled():
return settings.AUTH_TEMP_TOKEN

View File

@@ -22,5 +22,6 @@ class ConfirmMFA(BaseConfirm):
def authenticate(self, secret_key, mfa_type):
mfa_backend = self.user.get_mfa_backend_by_type(mfa_type)
mfa_backend.set_request(self.request)
ok, msg = mfa_backend.check_code(secret_key)
return ok, msg

View File

@@ -2,7 +2,7 @@ from django.db.models import TextChoices
from authentication.confirm import CONFIRM_BACKENDS
from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin
from .mfa import MFAOtp, MFASms, MFARadius, MFACustom
from .mfa import MFAOtp, MFASms, MFARadius, MFAFace, MFACustom
RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key'
@@ -35,5 +35,17 @@ class ConfirmType(TextChoices):
class MFAType(TextChoices):
OTP = MFAOtp.name, MFAOtp.display_name
SMS = MFASms.name, MFASms.display_name
Face = MFAFace.name, MFAFace.display_name
Radius = MFARadius.name, MFARadius.display_name
Custom = MFACustom.name, MFACustom.display_name
FACE_CONTEXT_CACHE_KEY_PREFIX = "FACE_CONTEXT"
FACE_CONTEXT_CACHE_TTL = 60
FACE_SESSION_KEY = "face_token"
class FaceMonitorActionChoices(TextChoices):
Verify = 'verify', 'verify'
Pause = 'pause', 'pause'
Resume = 'resume', 'resume'

View File

@@ -2,3 +2,4 @@ from .otp import MFAOtp, otp_failed_msg
from .sms import MFASms
from .radius import MFARadius
from .custom import MFACustom
from .face import MFAFace

View File

@@ -12,10 +12,14 @@ class BaseMFA(abc.ABC):
因为首页登录时,可能没法获取到一些状态
"""
self.user = user
self.request = None
def is_authenticated(self):
return self.user and self.user.is_authenticated
def set_request(self, request):
self.request = request
@property
@abc.abstractmethod
def name(self):

View File

@@ -0,0 +1,59 @@
from authentication.mfa.base import BaseMFA
from django.utils.translation import gettext_lazy as _
from authentication.mixins import AuthFaceMixin
from common.const import LicenseEditionChoices
from settings.api import settings
class MFAFace(BaseMFA, AuthFaceMixin):
name = "face"
display_name = _('Face Recognition')
placeholder = 'Face Recognition'
def check_code(self, code):
assert self.is_authenticated()
try:
code = self.get_face_code()
if not self.user.check_face(code):
return False, _('Facial comparison failed')
except Exception as e:
return False, "{}:{}".format(_('Facial comparison failed'), str(e))
return True, ''
def is_active(self):
if not self.is_authenticated():
return True
return bool(self.user.face_vector)
@staticmethod
def global_enabled():
return (
settings.XPACK_LICENSE_IS_VALID and
settings.XPACK_LICENSE_EDITION_ULTIMATE and
settings.FACE_RECOGNITION_ENABLED
)
def get_enable_url(self) -> str:
return '/ui/#/profile/index'
def get_disable_url(self) -> str:
return '/ui/#/profile/index'
def disable(self):
assert self.is_authenticated()
self.user.face_vector = ''
self.user.save(update_fields=['face_vector'])
def can_disable(self) -> bool:
return True
@staticmethod
def help_text_of_enable():
return _("Bind face to enable")
@staticmethod
def help_text_of_disable():
return _("Unbind face to disable")

View File

@@ -12,7 +12,7 @@ class MFARadius(BaseMFA):
display_name = 'Radius'
placeholder = _("Radius verification code")
def check_code(self, code):
def check_code(self, code=None):
assert self.is_authenticated()
backend = RadiusBackend()
username = self.user.username

View File

@@ -2,6 +2,7 @@ from django.conf import settings
from django.utils.translation import gettext_lazy as _
from common.utils.verify_code import SendAndVerifyCodeUtil
from users.serializers import SmsUserSerializer
from .base import BaseMFA
sms_failed_msg = _("SMS verify code invalid")
@@ -14,8 +15,13 @@ class MFASms(BaseMFA):
def __init__(self, user):
super().__init__(user)
phone = user.phone if self.is_authenticated() else ''
self.sms = SendAndVerifyCodeUtil(phone, backend=self.name)
phone, user_info = '', None
if self.is_authenticated():
phone = user.phone
user_info = SmsUserSerializer(user).data
self.sms = SendAndVerifyCodeUtil(
phone, backend=self.name, user_info=user_info
)
def check_code(self, code):
assert self.is_authenticated()

View File

@@ -2,6 +2,7 @@ import base64
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.http import HttpResponse
from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
@@ -34,7 +35,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
'login/mfa', 'mfa/select', 'face/context','jsi18n/', '/static/',
'/profile/otp', '/logout/',
]
for url in white_urls:
@@ -116,23 +117,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
@staticmethod
def set_cookie_public_key(request, response):
def set_cookie_public_key(self, request, response):
if request.path.startswith('/api'):
return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
session_public_key = request.session.get(session_public_key_name)
cookie_public_key = request.COOKIES.get(session_public_key_name)
if session_public_key and session_public_key == cookie_public_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
private_key, public_key = self.get_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
request.session[session_public_key_name] = public_key_decode
request.session[session_private_key_name] = private_key
response.set_cookie(session_public_key_name, public_key_decode)
def get_key_pair(self):
key_pair = cache.get(self.USER_LOGIN_ENCRYPTION_KEY_PAIR)
if key_pair:
return key_pair['private_key'], key_pair['public_key']
private_key, public_key = gen_key_pair()
key_pair = {
'private_key': private_key,
'public_key': public_key
}
cache.set(self.USER_LOGIN_ENCRYPTION_KEY_PAIR, key_pair, None)
return private_key, public_key
@staticmethod
def set_cookie_session_prefix(request, response):

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-12-12 06:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0003_sshkey'),
]
operations = [
migrations.AlterField(
model_name='passkey',
name='token',
field=models.CharField(max_length=1024, verbose_name='Token'),
),
]

Some files were not shown because too many files have changed in this diff Show More