Compare commits

...

440 Commits

Author SHA1 Message Date
Bai
8ebcfb5b6f perf: aks encrypt 2025-09-03 11:25:59 +08:00
ibuler
000bb100cd perf: try to decrypt then origin value 2025-08-29 11:00:19 +08:00
wangruidong
36f3071eed fix: Ensure command arguments are safely quoted in safe_run_cmd 2025-08-28 14:14:36 +08:00
老广
15259fc10c Update base.py 2025-08-21 22:05:51 +08:00
Bryan
f31994fdcd Merge pull request #15899 from jumpserver/dev 2025-08-21 19:03:18 +08:00
feng
b65ff0d84c perf: Translate 2025-08-21 18:52:38 +08:00
wangruidong
30d781dd12 fix: Export PDF wait for render done 2025-08-21 18:44:09 +08:00
wangruidong
9551cd4da9 fix: Export PDF with org id 2025-08-21 17:56:26 +08:00
mikebofs
87b456c941 perf: change default width 2025-08-21 16:19:56 +08:00
mikebofs
d4d5224c17 perf: support export dashboard 2025-08-21 16:19:56 +08:00
wangruidong
dabb30d90a perf: Change report name 2025-08-21 16:19:25 +08:00
feng
82192d38e1 perf: Translate 2025-08-21 15:32:04 +08:00
feng
571d2b4575 perf: Custom platform translate 2025-08-21 14:51:38 +08:00
Eric
ea64313c4e perf: fix conenct token platform fields 2025-08-21 14:03:15 +08:00
Bai
8764cdb733 feat: support protocols search 2025-08-21 11:49:18 +08:00
feng
980394efed perf: Transalte 2025-08-21 11:31:29 +08:00
wangruidong
2c94f10d64 fix: The approval setting org admin, and the approver is blank 2025-08-21 10:25:10 +08:00
wangruidong
e1c9f5180d perf: Export pdf using days parameter 2025-08-21 10:23:00 +08:00
wangruidong
3f1d7fa230 perf: Pdf file i18n 2025-08-21 10:23:00 +08:00
wangruidong
44bcd6e399 fix: Send email pdf deps 2025-08-21 10:23:00 +08:00
feng
5f87d98c31 perf: Translate 2025-08-20 18:17:46 +08:00
feng
540becdcbe perf: org admin view settings 2025-08-20 17:11:27 +08:00
feng
6929c4968e perf: Check api 2025-08-20 11:16:46 +08:00
Aaron3S
63b213d3a8 feat: add translate 2025-08-19 19:19:23 +08:00
feng
64fe7a55ec perf: Mongodb ping 2025-08-19 19:08:52 +08:00
feng
27829e09ef perf: Translate 2025-08-19 18:57:23 +08:00
jiangweidong
1bfc7daef6 perf: Avoid Oracle password modification SQL injection risks 2025-08-19 18:55:46 +08:00
Bai
9422aebc5e perf: email i18n 2025-08-19 18:49:25 +08:00
wangruidong
8c0cd20b48 fix: Disable passkey mfa in safe mode 2025-08-19 18:21:33 +08:00
Bai
0c612648a0 perf: email protocol rename 2025-08-19 17:04:32 +08:00
feng
36e01a316c perf: Regular command groups can be filled in with new lines 2025-08-19 15:51:39 +08:00
feng
e1b96e01eb perf: Translate 2025-08-19 15:05:13 +08:00
wangruidong
144f4b4466 fix: Virtual apps manifest i18n 2025-08-19 14:54:03 +08:00
wangruidong
8e007004c2 perf: Translate label for groups parameter 2025-08-19 14:51:52 +08:00
github-actions[bot]
c14f740209 perf: Update Dockerfile with new base image tag 2025-08-19 14:50:45 +08:00
Eric
13a85f062c perf: fix uv pip resolution 2025-08-19 14:50:45 +08:00
fit2bot
7f9d027bd3 perf: Send command translate (#15820)
Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: Bryan <jiangjie.bai@fit2cloud.com>
2025-08-18 19:14:48 +08:00
wangruidong
c037ce1c29 perf: Send report email 2025-08-18 19:12:29 +08:00
wangruidong
ee7c6b4708 fix: Init db error 2025-08-18 19:11:59 +08:00
feng
d0e625e322 perf: Translate 2025-08-18 19:08:34 +08:00
feng
c65794a99d perf: KOKO translate 2025-08-18 18:39:42 +08:00
Eric
1e4bca6e24 perf: add lion i18n 2025-08-18 18:28:22 +08:00
feng
c1c5025fbb perf: Account automation report 2025-08-18 17:40:49 +08:00
Eric
96020fa6b4 perf: add lion i18n 2025-08-18 11:42:33 +08:00
wangruidong
5ad6f87a9e fix: Docker build error 2025-08-18 10:53:33 +08:00
feng
9b0c73c9f9 perf: translate 2025-08-15 18:57:46 +08:00
wangruidong
c029714ffd fix: Export pdf failed 2025-08-15 17:42:48 +08:00
wangruidong
c1e8a1b561 fix: Install export pdf deps 2025-08-15 17:42:48 +08:00
feng
21126de2c1 perf: get_cpu_model_count 2025-08-15 16:45:39 +08:00
feng
7d06819bbe perf: foot_js 2025-08-15 16:35:43 +08:00
Eric
92b20fe2ef perf: add lion i18n 2025-08-15 16:24:18 +08:00
feng
4326d35065 perf: User report 2025-08-14 18:55:15 +08:00
feng
4810eae725 perf: group_stats 2025-08-14 16:09:43 +08:00
fit2bot
24f7946b7b perf: change some field to encrypt field (#15842)
* perf: conn token add remote addr

* perf: change some field to encrypt field

---------

Co-authored-by: ibuler <ibuler@qq.com>
2025-08-14 15:05:18 +08:00
王晓阳
4b9c4a550e feat: support vastbase 2025-08-14 14:31:31 +08:00
feng
d3ec23ba85 perf: group_stats 2025-08-14 11:45:36 +08:00
feng
e3c33bca32 perf: User report 2025-08-14 11:12:58 +08:00
feng
0fb7e84678 perf: user asset account report 2025-08-13 18:51:08 +08:00
feng
ab30bfb2d2 perf: mysql pg playbook 2025-08-13 15:15:53 +08:00
feng
d9d034488f fix: report 2025-08-12 19:19:00 +08:00
feng
24bd7b7e1a fix rbac pam 2025-08-12 14:48:16 +08:00
wangruidong
7fb5fd3956 fix: set ansible_timeout for account connectivity tasks 2025-08-11 10:37:23 +08:00
feng
9c621f5ff5 perf: rbac pam 2025-08-08 13:52:38 +08:00
feng
ac8998b9ee perf: Account risk delete normal account 2025-08-06 17:02:53 +08:00
wangruidong
b258537890 fix: Fallback to browser language if user language is not set 2025-08-06 14:15:30 +08:00
fit2bot
b38d83c578 feat: report charts (#15630)
* perf: initial

* perf: basic finished

* perf: depend

* perf: Update Dockerfile with new base image tag

* perf: Add user report api

* perf: Update Dockerfile with new base image tag

* perf: Use user report api

* perf: Update Dockerfile with new base image tag

* perf: user login report

* perf: Update Dockerfile with new base image tag

* perf: user change password

* perf: change password dashboard

* perf: Update Dockerfile with new base image tag

* perf: Translate

* perf: asset api

* perf: asset activity

* perf: Asset report

* perf: add charts_map

* perf: account report

* perf: Translate

* perf: account automation

* perf: Account automation

* perf: title

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng <1304903146@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
2025-08-06 14:05:38 +08:00
feng
257f290d18 perf: Translate 2025-08-06 11:33:52 +08:00
wangruidong
d185be2180 perf: Optimize redis connection number 2025-08-04 18:53:34 +08:00
ibuler
4e33b5b478 perf: some risk example file path 2025-08-01 10:35:15 +08:00
wangruidong
1406437d4e fix: Failed to switch languages 2025-08-01 10:24:17 +08:00
feng
e46aa95980 perf: check_asset_permission_will_expired filter is_active=True 2025-08-01 10:18:52 +08:00
Eric
c619a35a04 perf: update lion i18n tip 2025-08-01 10:18:12 +08:00
wangruidong
29f10bf10e perf: ES connect error detail 2025-07-31 17:15:55 +08:00
wangruidong
a822905ae7 fix: When the cas user doesn't exist, you will be prompted with an error when logging in. 2025-07-31 17:15:18 +08:00
zhaojisen
dc5a743f4f revert style 2025-07-30 14:27:52 +08:00
zhaojisen
1de8781704 Fixed: Fix the issue with the login page footer 2025-07-30 14:27:52 +08:00
wangruidong
f3d9f4c446 fix: Failed to switch languages 2025-07-29 16:40:30 +08:00
jiangweidong
6b5d5c15ae feat: Add an embedded form to ChatAI 2025-07-29 14:15:01 +08:00
feng
1074a0df19 perf: MFA coce reuse 2025-07-29 11:00:39 +08:00
Eric
04dca794dd fix: fix chrome_app password_manager dialog 2025-07-29 10:21:46 +08:00
ibuler
14e0396508 perf: change ip db path 2025-07-29 10:20:37 +08:00
wangruidong
835eb2e3d0 perf: Improve error handling for email sending in tasks 2025-07-28 10:30:42 +08:00
ibuler
be24f28d9b perf: in safe mode passkey cannot be as mfa 2025-07-25 10:50:46 +08:00
wangruidong
26cea550c4 fix: The applet list is not translated. 2025-07-25 10:49:47 +08:00
wangruidong
36ae076cb0 fix: Open redirect security vulnerability 2025-07-24 15:50:05 +08:00
feng
51c5294fb4 perf: Ticket filter org 2025-07-24 14:36:15 +08:00
feng
da083fffa3 perf: Translate email help text 2025-07-24 14:35:21 +08:00
feng
1df04d2a94 perf: Pam rbac 2025-07-23 10:21:38 +08:00
Eric
299e52cd11 perf: vnc_guide method only by xpack 2025-07-22 14:37:38 +08:00
feng
38b268b104 fix: Circular import 2025-07-22 14:36:22 +08:00
wangruidong
6095e9c9bd perf: Modify the layout to flex 2025-07-22 14:35:05 +08:00
ibuler
c4a348aac6 perf: remove client redirect api 2025-07-22 14:34:11 +08:00
feng
75575af56f perf: Callback client 2025-07-22 13:51:08 +08:00
Bryan
71766418bb Merge pull request #15742 from jumpserver/dev
merge: v4.10.4-lts
2025-07-17 15:12:58 +08:00
feng
8f91cb1473 perf: Translate 2025-07-17 15:12:01 +08:00
feng
b72e8eba7c perf: Change the secret and retry in batches 2025-07-17 14:21:31 +08:00
feng
d1d6f3fe9c perf: string_punctuation remove > ^ 2025-07-17 14:02:19 +08:00
wangruidong
6095c9865f fix: Action tips translate 2025-07-17 11:48:28 +08:00
wangruidong
6c374cb41f fix: View replay generate multiple operation logs 2025-07-17 11:24:16 +08:00
Eric
df64145adc perf: lion i18n 2025-07-16 19:49:07 +08:00
ibuler
44d77ba03f perf: random password exclude some char 2025-07-16 19:35:04 +08:00
wangruidong
3af188492f fix: Gather account failed 2025-07-16 19:19:43 +08:00
feng
9e798cd0b6 perf: Translate and tools version 2025-07-16 17:43:35 +08:00
feng
4d22c0722b fix: Exclude special char failed 2025-07-16 16:10:17 +08:00
Eric
e6a1662780 perf: add lion i18n 2025-07-15 18:58:42 +08:00
wangruidong
cc4be36752 perf: Log IntegrityError details during user authentication 2025-07-15 18:58:16 +08:00
wangruidong
e1f5d3c737 fix: Delete user failed(DoesNotExist) when user create share session 2025-07-15 18:43:43 +08:00
wangruidong
c0adc1fe74 fix: Gather account error 2025-07-15 18:43:14 +08:00
feng
613715135b perf: Translate 2025-07-15 11:46:39 +08:00
Eric
fe1d5f9828 perf: add en i18n 2025-07-11 15:34:24 +08:00
Eric
1d375e15c5 perf: add i18n keys 2025-07-11 15:34:24 +08:00
Eric
ac21d260ea perf: add lion i18n 2025-07-11 15:34:24 +08:00
wangruidong
accde77307 fix: Add third party login check is block 2025-07-11 15:33:48 +08:00
ibuler
c7dcf1ba59 perf: playbook task db save if conn timeout 2025-07-11 11:00:20 +08:00
wangruidong
b564bbebb3 perf: Translate 2025-07-11 10:30:40 +08:00
Eric
9440c855f4 perf: add lion i18n 2025-07-10 12:50:11 +08:00
w940853815
f282b2079e Update comment 2025-07-10 11:39:37 +08:00
wangruidong
1790cd8345 fix: Add additional third-party authentication backends and adjust MFA check 2025-07-10 11:39:37 +08:00
ibuler
7da74dc6e8 fix: integrate with azure oidc 2025-07-10 11:33:41 +08:00
Ewall555
33b0068f49 feat: exclude SSO token permissions for change and delete actions 2025-07-10 11:29:18 +08:00
Ewall555
9a446c118b feat: support rbac SSO token 2025-07-10 11:29:18 +08:00
Eric
4bf337b2b4 perf: add VNC terminal type 2025-07-10 11:28:32 +08:00
wangruidong
2acbb80920 perf: Add account date_expired 2025-07-09 10:47:06 +08:00
gerry-f2c
ae859c5562 perf: dbeaver uses a fixed driver directory (#15689) 2025-07-08 18:02:24 +08:00
Eric
a9bc716af5 perf: add encrypt field for sqlserver 2008 2025-07-08 18:01:31 +08:00
feng
2d5401e76e perf: Translate 2025-07-08 16:01:52 +08:00
Gerry.tan
d933e296bc perf: ES command log supports fuzzy search 2025-07-08 11:25:44 +08:00
wangruidong
1e5a995917 fix: Ticket filter error 2025-07-08 10:42:40 +08:00
wangruidong
baaaf83ab9 perf: Translate 2025-07-08 10:35:04 +08:00
wangruidong
ab06ac1f1f perf: Update IP group validation to include address validation 2025-07-08 10:34:34 +08:00
jiangweidong
99c4622ccb fix: SSO access to web assets with encrypted password auto-filling 2025-07-08 10:19:32 +08:00
Eric
9bdfab966f perf: add replay_size on session 2025-07-08 10:18:54 +08:00
老广
1a1acb62de Update README.md 2025-07-08 10:16:43 +08:00
wanghe-fit2cloud
2a128ea01b docs: Add GitCode badges 2025-07-07 15:33:08 +08:00
王贺
5a720b41bf docs: Add GitCode badge 2025-07-07 13:42:15 +08:00
feng
726c5cf34d fix: View replay record operate log 2025-07-07 10:37:29 +08:00
wangruidong
06afc8a0e1 perf: Translate 2025-07-02 19:04:15 +08:00
ibuler
276fd928a7 perf: add pg client 2025-07-01 16:18:25 +08:00
dependabot[bot]
05c6272d7e chore(deps): bump requests from 2.31.0 to 2.32.4
Bumps [requests](https://github.com/psf/requests) from 2.31.0 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 15:59:44 +08:00
Bai
c3f877d116 fix: check count 2025-07-01 15:35:17 +08:00
wangruidong
60deef2abf fix: Org admin cannot use system tools. 2025-07-01 15:23:34 +08:00
wangruidong
058754dc1b perf: Translate time cost 2025-06-30 10:05:13 +08:00
feng
a238c5d34b perf: operate record 2025-06-27 19:03:24 +08:00
feng626
76c6ed0f95 Merge pull request #15649 from jumpserver/pr@dev@translate
perf: Translate
2025-06-27 14:01:16 +08:00
feng626
0d07f7421b Merge branch 'dev' into pr@dev@translate 2025-06-27 14:01:05 +08:00
Ewall555
b07d4e207c perf: Translate 2025-06-27 13:59:57 +08:00
feng
dc92963059 perf: Translate 2025-06-27 13:15:07 +08:00
feng
9abd708a0a fix: ES search session count 2025-06-27 10:32:28 +08:00
jiangweidong
c9270877eb fix: According to the CMPP2.0 protocol standard, modify the attribute alignment. 2025-06-26 18:41:30 +08:00
feng
b5518dd2ba perf: Pam perm tree 2025-06-26 18:14:57 +08:00
wangruidong
1d40f5ecbc fix: Handle ValidationError in account_obj property 2025-06-26 15:23:16 +08:00
ibuler
91fee6c034 perf: change some 18n 2025-06-26 14:54:06 +08:00
feng
1b65055c5e perf: Account backup backup_type limit 2025-06-23 18:27:27 +08:00
feng
e79ef516a5 perf: Change secret windoes translate 2025-06-23 15:59:55 +08:00
Ewall555
8843f247d6 fix: Use local Python interpreter variable in RDP automation scripts 2025-06-23 14:15:46 +08:00
ibuler
cb42df542d fix: bitwardne request data encode 2025-06-23 14:13:15 +08:00
Ewall555
46ddad1d59 perf: Update metismenu plugin to version 3.0.7 2025-06-23 14:11:23 +08:00
Bryan
a9399dd709 Merge pull request #15608 from jumpserver/dev
v4.10.2
2025-06-19 20:14:21 +08:00
feng
f55a6ae364 perf: Translate 2025-06-19 20:09:06 +08:00
feng
34772b8fb4 fix: client login get session_key 2025-06-19 18:48:46 +08:00
feng
286891061b perf: face acl 2025-06-19 17:58:08 +08:00
feng
269bf1283e perf: Translate 2025-06-19 17:07:25 +08:00
Bai
d32c11bced fix: login page i18n same with personal settings 2025-06-19 16:47:53 +08:00
feng
fb64af2eb2 perf: Translate 2025-06-19 16:17:09 +08:00
Eric
82f32cbba3 perf: replace koko category to luna 2025-06-19 16:09:41 +08:00
wangruidong
411b485448 fix: The title of the risk detection email and server performance check notification i18n 2025-06-19 15:33:46 +08:00
feng
60608e92ea fix: Connection failure after directly connecting to asset ACL 2025-06-19 15:28:12 +08:00
feng
6922c62b50 perf: Translate 2025-06-19 11:49:47 +08:00
Eric
531c23d983 perf: add lina i18n 2025-06-19 10:22:24 +08:00
feng
df9e6cf866 perf: Translate 2025-06-18 16:37:33 +08:00
halo
65f2b92eb3 perf: Update client version and perf translate 2025-06-17 19:26:59 +08:00
feng626
bac621991e Merge pull request #15592 from jumpserver/pr@dev@translate
perf: Translate
2025-06-17 19:26:52 +08:00
feng626
db24d34b64 Merge branch 'dev' into pr@dev@translate 2025-06-17 19:25:59 +08:00
feng
265c066054 perf: Translate 2025-06-17 19:23:55 +08:00
feng
dad6b5def0 perf: Translate 2025-06-17 18:51:39 +08:00
feng
00f6c3a5de perf: Change secret record 2025-06-17 17:11:25 +08:00
wangruidong
cfbd162890 fix: Correct language retrieval in profile to use the provided object 2025-06-16 19:04:45 +08:00
wangruidong
17e8f25cb4 fix: Update language preference setting to include category 2025-06-16 19:04:45 +08:00
wangruidong
71bf8c8699 fix: The luna page cannot switch language settings 2025-06-16 19:04:45 +08:00
wangruidong
98342e0b70 fix: The login page cannot switch language settings 2025-06-16 19:04:45 +08:00
wangruidong
70aaa9cf8f fix: Activate user language when sending emails 2025-06-16 19:04:45 +08:00
Aaron3S
70b2d28760 feat: remove fuzzy 2025-06-16 18:40:18 +08:00
Aaron3S
8265a069e2 feat: translate 2025-06-16 18:40:18 +08:00
feng
9ec48aae0c perf: Translate 2025-06-16 14:55:23 +08:00
feng
41658af8fd perf: Suggestion api 2025-06-16 14:03:27 +08:00
fit2bot
7dfb31840e tinkner request ak first 2025-06-16 11:39:20 +08:00
ibuler
2f55db60ec perf: change redirect client auth to session 2025-06-13 10:41:28 +08:00
ibuler
551e6d0479 perf: client login redirect 2025-06-13 10:41:28 +08:00
feng
61c54314d7 perf: Face translate 2025-06-12 18:55:35 +08:00
wangruidong
4e7cd37c1d fix: Ensure user language is activated when sending notifications 2025-06-12 18:30:22 +08:00
wangruidong
e89f43dcd3 perf: Translate 2025-06-12 18:30:22 +08:00
wangruidong
259ead4c6e fix: Prevent nested resource issues in type nodes tree API 2025-06-12 18:29:22 +08:00
feng
348b2a833a perf: Translate 2025-06-12 18:28:33 +08:00
feng
8aec1604ce perf: Change secret clear account queue status 2025-06-12 16:55:04 +08:00
feng
be28a6954a perf: Login to change password and filter out useless accounts 2025-06-11 19:16:29 +08:00
feng
79c2284a01 perf: Change secret after successful login 2025-06-11 18:41:31 +08:00
feng
c2b44cfd84 perf: Translate 2025-06-11 16:36:55 +08:00
ibuler
1e07cba545 perf: open svc account register on deploy 2025-06-11 13:32:00 +08:00
ibuler
48a9b2664a perf: change ftplog asset length 2025-06-11 13:27:23 +08:00
ZhaoJiSen
b3bfbf5046 Merge pull request #15550 from jumpserver/pr@dev@send_mail_async
perf: send_mail_async func log subject recipients info
2025-06-11 11:17:25 +08:00
feng
08aa1e48b9 perf: send_mail_async func log subject recipients info 2025-06-11 11:16:03 +08:00
ZhaoJiSen
97d7427090 Merge pull request #15549 from jumpserver/pr@dev@translate
perf: Translate
2025-06-10 19:14:21 +08:00
feng
9f9d5855c4 perf: Translate 2025-06-10 19:12:31 +08:00
ZhaoJiSen
2db8f0f444 Merge pull request #15546 from jumpserver/pr@dev@send_mail_async
perf: send_mail_async add log
2025-06-10 17:47:22 +08:00
feng
b75210b0c3 perf: send_mail_async add log 2025-06-10 17:45:59 +08:00
wangruidong
4713c6ddf6 fix: Task search error 2025-06-10 16:58:37 +08:00
feng
b70fb58faf perf: Change secret after successful login 2025-06-10 16:57:28 +08:00
Aaron3S
3991976a00 feat: magnus support mongodb 2025-06-10 15:51:12 +08:00
ewall555
90256208dd perf: Update jsencrypt library version 2025-06-09 18:43:18 +08:00
wangruidong
bbd3b32aa1 perf: Remove username hint 2025-06-09 16:58:51 +08:00
wangruidong
ec20a4fd02 fix: Failed to update database assets 2025-06-09 15:04:27 +08:00
wangruidong
d179ce1cd4 perf: Add celery worker count config 2025-06-09 14:02:31 +08:00
ZhaoJiSen
caf23f5b05 Merge pull request #15529 from jumpserver/pr@dev@translate
perf: Translate
2025-06-06 18:23:10 +08:00
feng626
4bb19d59ef Merge branch 'dev' into pr@dev@translate 2025-06-06 18:22:24 +08:00
feng
74ed693a95 perf: Translate 2025-06-06 18:19:44 +08:00
feng
4a7a1fd95c perf: Optimize the results returned by the suggestion api for different organizations 2025-06-06 18:09:05 +08:00
wangruidong
56268433e0 perf: Translate adhoc 2025-06-06 17:56:53 +08:00
ibuler
ea59677b13 perf: swagger auth required 2025-06-06 17:56:25 +08:00
ibuler
94ed26e115 perf: change i18n 2025-06-06 17:56:25 +08:00
wangruidong
284d793253 perf: leak password can bulk delete 2025-06-06 17:05:13 +08:00
wangruidong
570566d9dd perf: set ansible_timeout for account connectivity tasks 2025-06-04 18:41:41 +08:00
wangruidong
3f85c67aee perf: Add retention period for expired user tokens and implement cleanup task 2025-06-04 18:39:49 +08:00
wangruidong
53a84850dc fix: Ensure platform_id is a digit before querying Platform 2025-06-04 18:37:05 +08:00
feng
e4be9621bb perf: Custom push account 2025-06-03 14:52:06 +08:00
Eric
f8b778ada2 perf: tinker to v0.2.2 2025-06-03 13:54:21 +08:00
fit2bot
5c28b15e39 perf: update chrome applet to support language setting (#15509)
* perf: update chrome applet to support language setting

* perf: fix field name

---------

Co-authored-by: Eric <xplzv@126.com>
2025-06-03 13:54:04 +08:00
wangruidong
5e0babdba8 perf: Language settings in personal settings 2025-05-29 11:13:04 +08:00
feng
8a3acb649e fix: ES non-global organizations cannot be queried 2025-05-27 14:31:27 +08:00
Eric
1ade652381 perf: upgrade tinker to v0.2.1 2025-05-23 11:20:17 +08:00
github-actions[bot]
7472f83d7a Auto-translate README 2025-05-22 18:34:57 +08:00
Bai
c56a3d0a2e perf: add ko readme 2025-05-22 18:10:34 +08:00
feng
1a10225823 perf: view task log 2025-05-22 17:44:19 +08:00
feng
56c94d7b3c fix: The account suggestions api cannot find the account associated with the DS 2025-05-22 11:50:17 +08:00
ibuler
16e7a12974 perf: static file download and catch 2025-05-20 13:14:47 +08:00
ibuler
1364889083 fix: aggregate resource api 2025-05-20 13:14:21 +08:00
feng
4f19954640 perf: SSO add mfa 2025-05-20 13:12:13 +08:00
feng
1b2e376681 perf: Account list not display spec_info field 2025-05-19 16:13:36 +08:00
ibuler
14c5162153 perf: client auth changed 2025-05-19 11:27:31 +08:00
Bai
f9245e17cd perf: readme 2025-05-16 18:43:39 +08:00
Aaron3S
6bd1ec960b feat: add a new piico gm alg 2025-05-16 15:07:39 +08:00
ibuler
77cc02ae60 perf: change google authenticator apk download 2025-05-15 17:41:49 +08:00
Bryan
d0cb9e5432 Merge pull request #15412 from jumpserver/dev
v4.10.0
2025-05-15 17:11:43 +08:00
feng
9969395500 fix: perm del node and user group 500 2025-05-15 15:55:32 +08:00
wangruidong
e1f03a194b fix: Asset list gather account raise connection already closed 2025-05-15 15:48:19 +08:00
Eric
aa0125385a perf: fix NoneType error 2025-05-15 15:01:57 +08:00
feng
8e8579bebe perf: translate 2025-05-15 14:41:19 +08:00
feng
ad5ce5d4cf perf: translate 2025-05-15 14:01:51 +08:00
ibuler
4f009504ad perf: load custom protocols 2025-05-15 11:41:33 +08:00
ibuler
986bc926fc perf: iframe set to sameorigin 2025-05-14 19:29:16 +08:00
feng
6aafb0f01a perf: Translate 2025-05-14 18:18:45 +08:00
feng
43775096d1 perf: Login switch language 2025-05-14 17:40:09 +08:00
ibuler
f826f43495 perf: simplify db using 2025-05-14 15:11:46 +08:00
ibuler
e9ff988d8c perf: db connection close if needt 2025-05-14 14:43:14 +08:00
feng
a72e6456d9 perf: Connect method exclude face 2025-05-14 14:02:27 +08:00
ibuler
941a784a5b perf: 修改 migrations 2025-05-13 19:15:01 +08:00
ibuler
edaf9bb0b2 perf: domain enabled to gateway enabled 2025-05-13 16:36:56 +08:00
feng
e8ca177fe4 perf: translate 2025-05-13 14:50:14 +08:00
ewall555
a88ebeff15 feat: Set the default expiration days for adding user and asset permissions 2025-05-13 10:35:21 +08:00
ibuler
bd0c50a3e4 fix: account username has domain, then set again 2025-05-12 18:36:26 +08:00
feng
9f121723c4 perf: auditor add asset user view perm 2025-05-12 17:57:48 +08:00
feng
245ed79b17 perf: Translate 2025-05-12 17:05:37 +08:00
ibuler
01c07a834b perf: 修改 adhoc 翻译 2025-05-12 10:50:43 +08:00
ibuler
4fb61e0af6 perf: org id error 2025-05-12 10:24:33 +08:00
Emmanuel Ferdman
19b7be33ae Resolve warnings of logger library
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-05-12 09:51:30 +08:00
feng
7797c76032 perf: migrate 2025-05-09 18:25:41 +08:00
老广
eb777854d4 Pr@dev@fix django version (#15374)
* fix: Downgrade django and djangorestframework versions for compatibility

* perf: Update Dockerfile with new base image tag

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-09 17:50:11 +08:00
Eric
22d4bd5292 perf: optimize file removal 2025-05-09 17:21:24 +08:00
ibuler
47d0d8b7a0 perf: stop dep check 2025-05-09 17:20:33 +08:00
wangruidong
5e298e5749 fix: Downgrade django and djangorestframework versions for compatibility 2025-05-09 16:10:49 +08:00
w940853815
ad3ba5034e Revert "fix: Upgrading django version causes json serialization problems"
This reverts commit ad56845d89.
2025-05-09 16:04:58 +08:00
feng
f1a1c3660d perf: Luna en translate 2025-05-09 15:14:56 +08:00
feng
eaf5bc5eb3 perf: I18n api 2025-05-09 14:46:48 +08:00
feng
30e680ad91 fix: ES the calculated quantity is 0 2025-05-08 17:23:15 +08:00
wangruidong
c9f281e8f7 fix: Handle exceptions in leak password check 2025-05-08 17:13:23 +08:00
wangruidong
ad56845d89 fix: Upgrading django version causes json serialization problems 2025-05-08 17:12:53 +08:00
feng
c3dceec3c7 perf: Connectivity add rdp error 2025-05-08 16:07:30 +08:00
wangruidong
100dad75f1 fix: i18n error 2025-05-08 14:50:54 +08:00
feng
aa52060f24 perf: Upgrade bootstrap js to 5.3.6 2025-05-08 14:12:17 +08:00
wangruidong
089a5f50f4 feat: Add LeakPasswords config 2025-05-07 17:47:22 +08:00
feng626
0bdbb6fd84 Merge pull request #15352 from jumpserver/pr@dev@connectivity_choice
perf: Connectivity choice
2025-05-07 17:30:48 +08:00
feng626
dd5bcab4ff Merge branch 'dev' into pr@dev@connectivity_choice 2025-05-07 17:30:30 +08:00
feng
49f0e51769 perf: Connectivity choice 2025-05-07 17:25:10 +08:00
wangruidong
5577e39f21 perf: Support watermark customization 2025-05-07 16:52:58 +08:00
老广
e2830ecdd6 perf: passkey auth auto mfa 2025-05-07 16:24:39 +08:00
feng
8065e04f26 perf: Translate 2025-05-07 15:50:04 +08:00
feng
fe70b60e95 perf: Users lina translate 2025-05-07 11:31:22 +08:00
feng
1f7836353a perf: translate 2025-05-07 10:32:08 +08:00
feng
a4296b3129 perf: Clean push record period 2025-05-06 18:32:56 +08:00
feng
ffc92fa7b4 perf: Clean push record period 2025-05-06 18:22:54 +08:00
github-actions[bot]
f94e032858 perf: Update Dockerfile with new base image tag 2025-04-30 11:13:34 +08:00
ibuler
25429e30ba perf: update quirements 2025-04-30 11:13:34 +08:00
CaptainB
5c7d539c6f chore: Configure Dependabot to group Python dependencies 2025-04-30 11:04:21 +08:00
fit2bot
a8a6e03428 perf: update deps 2025-04-30 10:58:19 +08:00
wangruidong
71b9b2df74 fix: Add AdminConnectionToken to operate log exclude_models 2025-04-30 10:51:25 +08:00
Eric
f1bc69b253 perf: add luna i18n 2025-04-29 18:12:10 +08:00
wangruidong
282ca25504 perf: Skip alert if login city seen in past 7 days 2025-04-29 17:46:10 +08:00
fit2bot
1bb44e783a perf: some i18n (#15312)
Co-authored-by: ibuler <ibuler@qq.com>
2025-04-29 17:45:29 +08:00
feng
a64fe4b0be perf: Account translate 2025-04-29 14:44:22 +08:00
feng
a75faf8da6 perf: Discover account translate 2025-04-29 14:36:13 +08:00
Bryan
537a9325a3 Update README.md (#15305)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md
2025-04-28 11:36:05 +08:00
feng
a9d455e867 perf: ntlm_err 2025-04-27 18:53:43 +08:00
feng
d06d26ac54 perf: Display asset/account connectivity error message 2025-04-27 18:50:00 +08:00
fit2bot
e992c44e11 perf: change lfs files download (#15293)
* perf: change lfs files download

* perf: clean unused ansible module

* perf: update lfs download

* perf: Update Dockerfile with new base image tag

* perf: change download path

* 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>
2025-04-27 14:35:10 +08:00
feng
24fe058fd9 perf: lina translate 2025-04-25 18:07:09 +08:00
feng
a3fef9cc54 perf: Update the key when the integration-application is updated or created 2025-04-25 17:29:44 +08:00
ibuler
471053e62a perf: change mcp integrate 2025-04-25 17:28:03 +08:00
jiangweidong
dc6308b030 perf: if the apply-asset-ticket name is 128 characters long, will raise 500 2025-04-25 17:27:13 +08:00
feng
f016ae6161 perf: add sftplog command models field index 2025-04-25 15:21:55 +08:00
feng
14a8d877e0 perf: ko translate 2025-04-25 15:04:03 +08:00
feng
ddf20570a1 perf: device support ad 2025-04-23 19:38:01 +08:00
feng
1ad9616b7f perf: gather facts gpu info 2025-04-22 17:48:21 +08:00
刘瑞斌
d7bc6bb201 chore: use uv as package-ecosystem 2025-04-21 13:36:24 +08:00
feng
f855043468 perf: luna ru translate 2025-04-21 11:34:51 +08:00
fit2bot
3159a4e794 perf: change domain to zone (#15255)
* perf: change domain to zone

* perf: change domain to zone

* perf: change some word

* perf: update gateway enabled i18n

* perf: change migrations

---------

Co-authored-by: ibuler <ibuler@qq.com>
2025-04-21 10:30:18 +08:00
feng
57fcebfdd3 fix: No data found for the carrying organization 2025-04-18 16:50:07 +08:00
feng626
c500bb4e4c Revert "Revert "perf:Stored command records in ES support accurate searching.""
This reverts commit 6bc1c5bd50.
2025-04-18 16:50:07 +08:00
feng
fd062b0da6 perf: ru translate 2025-04-18 14:52:44 +08:00
ibuler
bcb112d5c6 perf: user profile api 2025-04-18 14:11:56 +08:00
fit2bot
533dbf316c perf: add ali rds dependencies (#15247)
* perf: add ali rds dependencies

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: Eric <xplzv@126.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-04-18 12:11:51 +08:00
github-actions[bot]
9cce94b709 perf: Update Dockerfile with new base image tag 2025-04-18 11:53:40 +08:00
Eric
8b815d812b perf: modify Dockerfile-base 2025-04-18 11:53:40 +08:00
github-actions[bot]
a168fc8a62 perf: Update Dockerfile with new base image tag 2025-04-18 11:29:07 +08:00
Eric
faae1a09d1 perf: lint dependencies 2025-04-18 11:29:07 +08:00
github-actions[bot]
26e819e120 perf: Update Dockerfile with new base image tag 2025-04-18 11:29:07 +08:00
Eric
79579654a1 perf: use uv tool
perf: add Homepage
perf: add env
2025-04-18 11:29:07 +08:00
老广
558188da90 merge: dev to master
Ready to relase
2025-04-17 20:24:45 +08:00
feng626
6bc1c5bd50 Revert "perf:Stored command records in ES support accurate searching."
This reverts commit 3d6d2af268.
2025-04-17 20:16:06 +08:00
ibuler
36f312b943 perf: page queryset mixin 2025-04-17 19:47:51 +08:00
ibuler
11811c453b perf: page queryset mixin 2025-04-17 19:47:51 +08:00
ibuler
12fadeec58 perf: revert terminal api 2025-04-17 19:34:23 +08:00
ibuler
b49fd21e08 perf: 虚拟账号 api 2025-04-17 17:50:42 +08:00
feng
9b982eb592 perf: change secret api perm 2025-04-17 17:15:06 +08:00
wangruidong
31652ef5b1 fix: include openid in source validation logic 2025-04-17 16:24:00 +08:00
feng
8fef18b991 perf: gather account windows playbook failed_when: false 2025-04-17 15:55:49 +08:00
ibuler
c804c053d2 perf: revert api 2025-04-17 15:53:52 +08:00
ibuler
bef2282604 perf: asset list compute account amount 2025-04-17 15:15:36 +08:00
feng
cabc069045 perf: Translate 2025-04-17 15:12:16 +08:00
feng
99c9a021b7 fix: update applet host (platform failed) 2025-04-17 14:55:36 +08:00
ibuler
6cb3cc1f29 perf: 修改 DS 的一些翻译 2025-04-17 14:47:11 +08:00
feng
67422ef4ba fix: automation no account 2025-04-17 14:00:24 +08:00
gerry
3d6d2af268 perf:Stored command records in ES support accurate searching. 2025-04-17 11:43:16 +08:00
wangruidong
ee97e45cc3 fix: Allow superusers delete adhoc and playbook 2025-04-17 10:54:47 +08:00
feng
0131eaa6db perf: es search 2025-04-16 18:15:58 +08:00
feng
eaa390fd6f perf: update asset directory_services allow_empty true 2025-04-16 17:39:55 +08:00
ibuler
e2b8fd0d40 perf: change account filter by asset 2025-04-16 17:37:36 +08:00
feng
2aace05099 perf: as account username 2025-04-16 17:29:23 +08:00
ibuler
1ee70af93d perf: applet account select 2025-04-16 16:43:34 +08:00
feng
fa70fb2921 perf: Translate 2025-04-16 15:37:11 +08:00
ibuler
01a6019022 perf: swagger api 2025-04-16 14:53:51 +08:00
wangruidong
5c61a11d82 fix: add periodic_display to read_only_fields in Job serializer 2025-04-16 14:18:11 +08:00
fit2bot
67f3341310 perf: change db prefetch (#15215) 2025-04-16 13:48:12 +08:00
feng
cb49e26387 perf: refresh asset type tree 2025-04-16 11:44:07 +08:00
feng
314da330c0 perf: Asset account filter 2025-04-16 11:36:58 +08:00
halo
f1c98fda34 perf: client version 2025-04-16 10:39:10 +08:00
ibuler
1fdd1036d3 perf: directory service db 2025-04-15 20:24:10 +08:00
feng
e286997090 perf: koko translate 2025-04-15 17:26:06 +08:00
wangruidong
ce3daf5496 fix: update translation strings and improve error handling in inventory and job modules 2025-04-15 16:49:35 +08:00
feng
631570b819 perf: Asset filter 2025-04-15 16:45:50 +08:00
feng
9b1bff0847 perf: client version 2025-04-15 12:28:59 +08:00
feng
ee8a2afe16 fix: ES no data found 2025-04-15 12:07:56 +08:00
CaptainB
1a01c0537c chore: Add Dependabot configuration for pip dependencies 2025-04-15 11:47:59 +08:00
jiangweidong
64393fe695 fix: Error in using set method 2025-04-15 11:47:01 +08:00
jiangweidong
11ef4fab4e perf: Es subsequent optimization 2025-04-15 11:47:01 +08:00
jiangweidong
9f8256f885 fix: Solve the problem that log details cannot be viewed in non-default organizations 2025-04-15 11:47:01 +08:00
fit2bot
5390fbacec perf: some swagger api (#15203)
* perf: some swagger api

* perf: update deps

* 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>
2025-04-15 11:43:36 +08:00
wangruidong
8b9fe3c72b perf: add default logo handling in IntegrationApplicationSerializer 2025-04-15 11:08:15 +08:00
wangruidong
20070e0647 fix: improve crontab validation logic 2025-04-15 11:07:23 +08:00
ibuler
47b72cb35e perf: add leak password db config 2025-04-14 17:27:27 +08:00
feng
2ca0e9a5a2 perf: automation .account -> .all_accounts 2025-04-14 14:18:56 +08:00
feng
3b2ac101c8 perf: windows ad asset info 2025-04-14 10:14:38 +08:00
feng
6795f036dd perf: ad asset automation gather_facts_enabled true 2025-04-11 17:53:43 +08:00
feng
aaa1f48258 perf: koko translate 2025-04-11 17:45:58 +08:00
feng
53c5bab203 perf: user login acl remove warning notify_and_warn action 2025-04-10 15:56:31 +08:00
feng
1254d28463 perf: windows ad gather account 2025-04-10 15:33:42 +08:00
wangruidong
d6b1a577fc fix: resolve ForeignKeyViolation in AccountRisk 2025-04-09 19:30:05 +08:00
wangruidong
5ab85d3561 perf: File directory adjustment 2025-04-09 19:29:52 +08:00
wangruidong
467f4c5d4f perf: Translate 2025-04-09 19:29:52 +08:00
wangruidong
f2404319af fix: account_prefer 2025-04-09 19:29:52 +08:00
wangruidong
bbeadf7dbe perf: optimize adhoc asset selection experience 2025-04-09 19:29:52 +08:00
feng
941bd9b3f4 perf: Translate 2025-04-09 19:22:32 +08:00
feng
37a307a9d0 perf: Windows AD 2025-04-09 18:22:00 +08:00
ibuler
528f9045d0 perf: update connection token 2025-04-09 11:11:39 +08:00
ibuler
a317549a01 perf: migrations merge 2025-04-08 19:21:37 +08:00
ibuler
0f5681de7d chore: remove workflow 2025-04-08 19:21:37 +08:00
ibuler
a7c514f8d8 perf: rename some workd 2025-04-08 19:21:37 +08:00
ibuler
75ea0079a2 perf: update ad domain 2025-04-08 19:21:37 +08:00
ibuler
4cc1687bf8 perf: update ad 2025-04-08 19:21:37 +08:00
ibuler
76e57b9a3e perf: update ad 2025-04-08 19:21:37 +08:00
ibuler
ba3bce1e2e perf: perm account valid 2025-04-08 19:21:37 +08:00
ibuler
45f0343cfa perf: update ds 2025-04-08 19:21:37 +08:00
ibuler
acaa4cf2d5 perf: rename ad to ds 2025-04-08 19:21:37 +08:00
ibuler
3f452daee8 perf: ad as asset 2025-04-08 19:21:37 +08:00
feng
5e25361ee8 perf: Operate default log 2025-04-08 18:37:25 +08:00
jiangweidong
7b7604e14d Added cloud sync global released asset tab (v4.9) 2025-04-08 18:31:53 +08:00
Eric
f9037878c3 perf: add remoteapp bitmapcache settings 2025-04-08 17:36:48 +08:00
fit2bot
29ddfcac17 fix: Optimize UserConfirmDialog to send code via email (#15164)
* fix: Optimize UserConfirmDialog to send code via email

* fix: Optimize verification failure without error reporting

---------

Co-authored-by: halo <wuyihuangw@gmail.com>
Co-authored-by: Bryan <jiangjie.bai@fit2cloud.com>
2025-04-08 15:50:15 +08:00
wangruidong
519ec65ad4 perf: LDAP strict sync 2025-04-08 15:43:15 +08:00
jiangweidong
1f60e328b6 perf: Export resources to add operation logs 2025-04-08 15:37:29 +08:00
Bai
e8e0ea920b perf: change jumpserver.org to jumpserver.com 2025-04-08 14:23:28 +08:00
Aaron3S
4fd8efd043 feat: remove oracle dyn port 2025-04-08 13:50:30 +08:00
wangruidong
623c800d31 fix: failure when deleting remote account in SQL Server 2025-04-08 13:48:48 +08:00
wangruidong
d2c6e3c7a6 fix: Job audit: Search job list, filter failures based on command 2025-04-08 13:41:59 +08:00
github-actions[bot]
dc5883576d Auto-translate README 2025-04-08 13:34:21 +08:00
Bai
0a9c9fb227 perf: change readme 2025-04-08 13:26:48 +08:00
Bai
15a1a58eca perf: change support email 2025-04-08 13:20:22 +08:00
halo
782401ef86 fix: Implement function _check_code 2025-04-07 16:20:43 +08:00
maninhill
8abcd201bc chore: Update README.md 2025-04-03 15:11:38 +08:00
Bryan
cdbc10ac72 Update README.md 2025-03-31 17:37:01 +08:00
Bryan
ceeef890e6 Update README.md 2025-03-31 17:37:01 +08:00
Bryan
dc8a172884 Update README.md 2025-03-28 17:53:22 +08:00
Bryan
62115e43bb Update README.md 2025-03-28 17:53:22 +08:00
Bryan
5eced85e69 Update README.md 2025-03-28 17:53:22 +08:00
Bryan
ec99b17b76 Update README.md 2025-03-28 17:53:22 +08:00
Bryan
84569720c3 Update README.md 2025-03-28 17:53:22 +08:00
feng
65984d38f1 perf: Account filter 2025-03-28 16:34:53 +08:00
feng
f6913ac63c perf: Priacy mode 2025-03-27 18:34:11 +08:00
Halo
514b2cdfc5 feat: Email as a method for multi-factor authentication (#15134)
* feat: Email as a method for multi-factor authentication

* perf: Optimize the MFA email sending logic

* perf: Optimize some parameters

* perf: Translate
2025-03-27 17:26:38 +08:00
ibuler
b55000663e perf: 提升服务注册安全性 2025-03-27 16:25:28 +08:00
ibuler
9ed822bb3e perf: 优化获取 labels 2025-03-27 16:23:14 +08:00
feng
ea599d7695 perf: Perm the template push account 2025-03-27 14:13:29 +08:00
feng
01c5d68b35 perf: change secret change_secret_result 2025-03-27 14:08:19 +08:00
feng
2e2c331941 perf: translate 2025-03-25 18:03:10 +08:00
feng
266ea9b858 perf: Change secret 2025-03-25 16:05:47 +08:00
feng
5f2e838342 perf: koko sftp translate 2025-03-25 14:33:50 +08:00
ZhaoJiSen
544ad5532b Merge pull request #15117 from jumpserver/pr@dev@koko_translate
perf: koko translate
2025-03-25 11:02:08 +08:00
feng
d22d715ee7 perf: koko translate 2025-03-25 10:59:45 +08:00
halo
dd2366532c perf: Use a domain account to avoid automatically creating a local account 2025-03-25 10:22:16 +08:00
ibuler
9667a3d340 perf: add crontab check min 60m 2025-03-25 09:56:37 +08:00
feng
c8e6e5d38c perf: Login language 2025-03-24 18:55:40 +08:00
feng
9d1047fae2 perf: Translate 2025-03-24 16:51:49 +08:00
feng
28f97d746d perf: Translate 2025-03-24 15:39:37 +08:00
ibuler
be72344c63 perf: update tk create 2025-03-24 10:54:17 +08:00
ibuler
d3176b68a8 perf: 优化 admin token 判断 2025-03-24 10:06:22 +08:00
ibuler
5411f65546 perf: update get permed account 2025-03-24 09:51:46 +08:00
ibuler
e3ba468004 fix: 修复 token 直连的问题 2025-03-24 09:51:46 +08:00
feng
a03a11efa4 perf: Translate 2025-03-21 15:49:42 +08:00
feng
d344495417 perf: Translate 2025-03-21 14:24:37 +08:00
github-actions[bot]
9412bd0331 Auto-translate README 2025-03-21 13:29:01 +08:00
Bai
8d73ddb1cd perf: update readme languages 2025-03-21 13:17:56 +08:00
Bai
7fe56a5e1a perf: README 2025-03-21 10:35:27 +08:00
513 changed files with 39569 additions and 24492 deletions

View File

@@ -8,4 +8,6 @@ celerybeat.pid
.vagrant/ .vagrant/
apps/xpack/.git apps/xpack/.git
.history/ .history/
.idea .idea
.venv/
.env

4
.gitattributes vendored
View File

@@ -1,4 +0,0 @@
*.mmdb filter=lfs diff=lfs merge=lfs -text
*.mo filter=lfs diff=lfs merge=lfs -text
*.ipdb filter=lfs diff=lfs merge=lfs -text
leak_passwords.db filter=lfs diff=lfs merge=lfs -text

14
.github/dependabot.yml.bak vendored Normal file
View File

@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:30"
timezone: "Asia/Shanghai"
target-branch: dev
groups:
python-dependencies:
patterns:
- "*"

View File

@@ -2,10 +2,14 @@ name: Translate README
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
source_readme:
description: "Source README"
required: false
default: "./readmes/README.en.md"
target_langs: target_langs:
description: "Target Languages" description: "Target Languages"
required: false required: false
default: "zh-hans,zh-hant,ja,pt-br" default: "zh-hans,zh-hant,ja,pt-br,es,ru"
gen_dir_path: gen_dir_path:
description: "Generate Dir Name" description: "Generate Dir Name"
required: false required: false
@@ -34,6 +38,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }} GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }} OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }}
GPT_MODE: ${{ github.event.inputs.gpt_mode }} GPT_MODE: ${{ github.event.inputs.gpt_mode }}
SOURCE_README: ${{ github.event.inputs.source_readme }}
TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }} TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }}
PUSH_BRANCH: ${{ github.event.inputs.push_branch }} PUSH_BRANCH: ${{ github.event.inputs.push_branch }}
GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }} GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }}

3
.gitignore vendored
View File

@@ -46,3 +46,6 @@ test.py
.test/ .test/
*.mo *.mo
apps.iml apps.iml
*.db
*.mmdb
*.ipdb

11
.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"endOfLine": "lf"
}

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20250224_065619 AS stage-build FROM jumpserver/core-base:20250819_064003 AS stage-build
ARG VERSION ARG VERSION

View File

@@ -1,6 +1,6 @@
FROM python:3.11-slim-bullseye FROM python:3.11-slim-bullseye
ARG TARGETARCH ARG TARGETARCH
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
# Install APT dependencies # Install APT dependencies
ARG DEPENDENCIES=" \ ARG DEPENDENCIES=" \
ca-certificates \ ca-certificates \
@@ -43,18 +43,19 @@ WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR} ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH
ENV UV_LINK_MODE=copy
RUN --mount=type=cache,target=/root/.cache \ 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=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \ --mount=type=bind,source=requirements/clean_site_packages.sh,target=clean_site_packages.sh \
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \ --mount=type=bind,source=requirements/collections.yml,target=collections.yml \
--mount=type=bind,source=requirements/static_files.sh,target=utils/static_files.sh \
set -ex \ set -ex \
&& python3 -m venv /opt/py3 \ && uv venv \
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \ && uv pip install -i${PIP_MIRROR} -r pyproject.toml \
&& . /opt/py3/bin/activate \ && ln -sf $(pwd)/.venv /opt/py3 \
&& poetry config virtualenvs.create false \ && bash utils/static_files.sh \
&& poetry install --no-cache --only main \ && bash clean_site_packages.sh
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
&& bash clean_site_packages.sh \
&& poetry cache clear pypi --all

View File

@@ -13,7 +13,9 @@ ARG TOOLS=" \
nmap \ nmap \
telnet \ telnet \
vim \ vim \
wget" postgresql-client-13 \
wget \
poppler-utils"
RUN set -ex \ RUN set -ex \
&& apt-get update \ && apt-get update \
@@ -24,11 +26,7 @@ RUN set -ex \
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple 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 poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& poetry install --only xpack \
&& poetry cache clear pypi --all
RUN set -ex \
&& uv pip install -i${PIP_MIRROR} --group xpack \
&& playwright install chromium --with-deps --only-shell

View File

@@ -1,16 +1,18 @@
<div align="center"> <div align="center">
<a name="readme-top"></a> <a name="readme-top"></a>
<a href="https://jumpserver.org/index-en.html"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a> <a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
## An open-source PAM tool (Bastion Host) ## An open-source PAM tool (Bastion Host)
[![][license-shield]][license-link] [![][license-shield]][license-link]
[![][docs-shield]][docs-link]
[![][deepwiki-shield]][deepwiki-link]
[![][discord-shield]][discord-link] [![][discord-shield]][discord-link]
[![][docker-shield]][docker-link] [![][docker-shield]][docker-link]
[![][github-release-shield]][github-release-link] [![][github-release-shield]][github-release-link]
[![][github-stars-shield]][github-stars-link] [![][github-stars-shield]][github-stars-link]
[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) [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) · [Español](/readmes/README.es.md) · [Русский](/readmes/README.ru.md) · [한국어](/readmes/README.ko.md)
</div> </div>
<br/> <br/>
@@ -19,7 +21,13 @@
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser. JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
![JumpServer Overview](https://github.com/jumpserver/jumpserver/assets/32935519/35a371cb-8590-40ed-88ec-f351f8cf9045)
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://www.jumpserver.com/images/jumpserver-arch-light.png">
<source media="(prefers-color-scheme: dark)" srcset="https://www.jumpserver.com/images/jumpserver-arch-dark.png">
<img src="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f" alt="Theme-based Image">
</picture>
## Quickstart ## Quickstart
@@ -36,18 +44,19 @@ Access JumpServer in your browser at `http://your-jumpserver-ip/`
[![JumpServer Quickstart](https://github.com/user-attachments/assets/0f32f52b-9935-485e-8534-336c63389612)](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart") [![JumpServer Quickstart](https://github.com/user-attachments/assets/0f32f52b-9935-485e-8534-336c63389612)](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
## Screenshots ## Screenshots
<table style="border-collapse: collapse; border: 1px solid black;"> <table style="border-collapse: collapse; border: 1px solid black;">
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/99fabe5b-0475-4a53-9116-4c370a1426c4" alt="JumpServer Console" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/99fabe5b-0475-4a53-9116-4c370a1426c4" alt="JumpServer Console" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/a424d731-1c70-4108-a7d8-5bbf387dda9a" alt="JumpServer Audits" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/7c1f81af-37e8-4f07-8ac9-182895e1062e" alt="JumpServer PAM" /></td>    
</tr> </tr>
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/a424d731-1c70-4108-a7d8-5bbf387dda9a" alt="JumpServer Audits" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/393d2c27-a2d0-4dea-882d-00ed509e00c9" alt="JumpServer Workbench" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/393d2c27-a2d0-4dea-882d-00ed509e00c9" alt="JumpServer Workbench" /></td>
</tr>
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/eaa41f66-8cc8-4f01-a001-0d258501f1c9" alt="JumpServer RBAC" /></td>     
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/3a2611cd-8902-49b8-b82b-2a6dac851f3e" alt="JumpServer Settings" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/3a2611cd-8902-49b8-b82b-2a6dac851f3e" alt="JumpServer Settings" /></td>
</tr> </tr>
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/1e236093-31f7-4563-8eb1-e36d865f1568" alt="JumpServer SSH" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/1e236093-31f7-4563-8eb1-e36d865f1568" alt="JumpServer SSH" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/69373a82-f7ab-41e8-b763-bbad2ba52167" alt="JumpServer RDP" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/69373a82-f7ab-41e8-b763-bbad2ba52167" alt="JumpServer RDP" /></td>
@@ -69,24 +78,20 @@ 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 | | [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 | | [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 | | [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 Remote Application Connector (Windows) |
| [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) | | [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector | | [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 | | [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 | | [Facelive](https://github.com/jumpserver/facelive) | <img alt="Facelive" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Facial Recognition |
## Third-party projects
- [jumpserver-grafana-dashboard](https://github.com/acerrah/jumpserver-grafana-dashboard) JumpServer with grafana dashboard
## Contributing ## Contributing
Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines. Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines.
## Security
JumpServer is a mission critical product. Please refer to the Basic Security Recommendations for installation and deployment. If you encounter any security-related issues, please contact us directly:
- Email: support@fit2cloud.com
## License ## License
Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved. Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved.
@@ -100,6 +105,7 @@ Unless required by applicable law or agreed to in writing, software distributed
<!-- JumpServer official link --> <!-- JumpServer official link -->
[docs-link]: https://jumpserver.com/docs [docs-link]: https://jumpserver.com/docs
[discord-link]: https://discord.com/invite/W6vYXmAQG2 [discord-link]: https://discord.com/invite/W6vYXmAQG2
[deepwiki-link]: https://deepwiki.com/jumpserver/jumpserver/
[contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md [contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
<!-- JumpServer Other link--> <!-- JumpServer Other link-->
@@ -110,10 +116,10 @@ Unless required by applicable law or agreed to in writing, software distributed
[github-issues-link]: https://github.com/jumpserver/jumpserver/issues [github-issues-link]: https://github.com/jumpserver/jumpserver/issues
<!-- Shield link--> <!-- Shield link-->
[docs-shield]: https://img.shields.io/badge/documentation-148F76
[github-release-shield]: https://img.shields.io/github/v/release/jumpserver/jumpserver [github-release-shield]: https://img.shields.io/github/v/release/jumpserver/jumpserver
[github-stars-shield]: https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square [github-stars-shield]: https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square   
[docker-shield]: https://img.shields.io/docker/pulls/jumpserver/jms_all.svg [docker-shield]: https://img.shields.io/docker/pulls/jumpserver/jms_all.svg
[license-shield]: https://img.shields.io/github/license/jumpserver/jumpserver [license-shield]: https://img.shields.io/github/license/jumpserver/jumpserver
[deepwiki-shield]: https://img.shields.io/badge/deepwiki-devin?color=blue
[discord-shield]: https://img.shields.io/discord/1194233267294052363?style=flat&logo=discord&logoColor=%23f5f5f5&labelColor=%235462eb&color=%235462eb [discord-shield]: https://img.shields.io/discord/1194233267294052363?style=flat&logo=discord&logoColor=%23f5f5f5&labelColor=%235462eb&color=%235462eb
<!-- Image link -->

View File

@@ -5,8 +5,7 @@ JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议
如果你发现安全问题,请直接联系我们,我们携手让世界更好: 如果你发现安全问题,请直接联系我们,我们携手让世界更好:
- ibuler@fit2cloud.com - ibuler@fit2cloud.com
- support@fit2cloud.com - support@lxware.hk
- 400-052-0755
# Security Policy # Security Policy
@@ -16,6 +15,5 @@ JumpServer is a security product, The installation and development should follow
All security bugs should be reported to the contact as below: All security bugs should be reported to the contact as below:
- ibuler@fit2cloud.com - ibuler@fit2cloud.com
- support@fit2cloud.com - support@lxware.hk
- 400-052-0755

View File

@@ -41,11 +41,21 @@ class AccountViewSet(OrgBulkModelViewSet):
'partial_update': ['accounts.change_account'], 'partial_update': ['accounts.change_account'],
'su_from_accounts': 'accounts.view_account', 'su_from_accounts': 'accounts.view_account',
'clear_secret': 'accounts.change_account', 'clear_secret': 'accounts.change_account',
'move_to_assets': 'accounts.create_account', 'move_to_assets': 'accounts.delete_account',
'copy_to_assets': 'accounts.create_account', 'copy_to_assets': 'accounts.add_account',
} }
export_as_zip = True export_as_zip = True
def get_queryset(self):
queryset = super().get_queryset()
asset_id = self.request.query_params.get('asset') or self.request.query_params.get('asset_id')
if not asset_id:
return queryset
asset = get_object_or_404(Asset, pk=asset_id)
queryset = asset.all_accounts.all()
return queryset
@action(methods=['get'], detail=False, url_path='su-from-accounts') @action(methods=['get'], detail=False, url_path='su-from-accounts')
def su_from_accounts(self, request, *args, **kwargs): def su_from_accounts(self, request, *args, **kwargs):
account_id = request.query_params.get('account') account_id = request.query_params.get('account')
@@ -68,18 +78,25 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes=[IsValidUser] permission_classes=[IsValidUser]
) )
def username_suggestions(self, request, *args, **kwargs): def username_suggestions(self, request, *args, **kwargs):
asset_ids = request.data.get('assets', []) raw_asset_ids = request.data.get('assets', [])
node_ids = request.data.get('nodes', []) node_ids = request.data.get('nodes', [])
username = request.data.get('username', '') username = request.data.get('username', '')
accounts = Account.objects.all() asset_ids = set(raw_asset_ids)
if node_ids: if node_ids:
nodes = Node.objects.filter(id__in=node_ids) nodes = Node.objects.filter(id__in=node_ids)
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) node_asset_qs = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
asset_ids.extend(node_asset_ids) asset_ids |= {str(u) for u in node_asset_qs}
if asset_ids: if asset_ids:
accounts = accounts.filter(asset_id__in=list(set(asset_ids))) through = Asset.directory_services.through
ds_qs = through.objects.filter(asset_id__in=asset_ids) \
.values_list('directoryservice_id', flat=True)
asset_ids |= {str(u) for u in ds_qs}
accounts = Account.objects.filter(asset_id__in=list(asset_ids))
else:
accounts = Account.objects.all()
if username: if username:
accounts = accounts.filter(username__icontains=username) accounts = accounts.filter(username__icontains=username)
@@ -117,7 +134,7 @@ class AccountViewSet(OrgBulkModelViewSet):
self.model.objects.create(**account_data) self.model.objects.create(**account_data)
success_count += 1 success_count += 1
except Exception as e: except Exception as e:
logger.debug(f'{ "Move" if move else "Copy" } to assets error: {e}') logger.debug(f'{"Move" if move else "Copy"} to assets error: {e}')
creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'} creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'}
results = [{'asset': str(asset), **res} for asset, res in creation_results.items()] results = [{'asset': str(asset), **res} for asset, res in creation_results.items()]

View File

@@ -62,8 +62,7 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
) )
def get_once_secret(self, request, *args, **kwargs): def get_once_secret(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
secret = instance.get_secret() return Response(data={'id': instance.id, 'secret': instance.secret})
return Response(data={'id': instance.id, 'secret': secret})
@action(['GET'], detail=False, url_path='account-secret', @action(['GET'], detail=False, url_path='account-secret',
permission_classes=[RBACPermission]) permission_classes=[RBACPermission])

View File

@@ -20,7 +20,7 @@ __all__ = ['PamDashboardApi']
class PamDashboardApi(APIView): class PamDashboardApi(APIView):
http_method_names = ['get'] http_method_names = ['get']
rbac_perms = { rbac_perms = {
'GET': 'accounts.view_account', 'GET': 'rbac.view_pam',
} }
@staticmethod @staticmethod

View File

@@ -43,6 +43,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
search_fields = ('username', 'name') search_fields = ('username', 'name')
serializer_classes = { serializer_classes = {
'default': serializers.AccountTemplateSerializer, 'default': serializers.AccountTemplateSerializer,
'retrieve': serializers.AccountDetailTemplateSerializer,
} }
rbac_perms = { rbac_perms = {
'su_from_account_templates': 'accounts.view_accounttemplate', 'su_from_account_templates': 'accounts.view_accounttemplate',

View File

@@ -17,7 +17,7 @@ from orgs.mixins import generics
__all__ = [ __all__ = [
'AutomationAssetsListApi', 'AutomationRemoveAssetApi', 'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi',
'AutomationExecutionViewSet', 'RecordListMixin' 'AutomationExecutionViewSet'
] ]
@@ -39,9 +39,10 @@ class AutomationAssetsListApi(generics.ListAPIView):
return assets return assets
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView): class AutomationRemoveAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@@ -56,9 +57,10 @@ class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
return Response({'msg': 'ok'}) return Response({'msg': 'ok'})
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView): class AutomationAddAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@@ -72,9 +74,10 @@ class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
return Response({"error": serializer.errors}) return Response({"error": serializer.errors})
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView): class AutomationNodeAddRemoveApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateNodeSerializer serializer_class = serializers.UpdateNodeSerializer
http_method_names = ['patch']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
action_params = ['add', 'remove'] action_params = ['add', 'remove']
@@ -124,12 +127,3 @@ class AutomationExecutionViewSet(
execution = self.get_object() execution = self.get_object()
report = execution.manager.gen_report() report = execution.manager.gen_report()
return HttpResponse(report) return HttpResponse(report)
class RecordListMixin:
def list(self, request, *args, **kwargs):
try:
response = super().list(request, *args, **kwargs)
except Exception as e:
response = Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return response

View File

@@ -6,24 +6,27 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice from accounts.const import (
from accounts.filters import ChangeSecretRecordFilterSet AutomationTypes, ChangeSecretRecordStatusChoice
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord )
from accounts.filters import ChangeSecretRecordFilterSet, ChangeSecretStatusFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, Account
from accounts.tasks import execute_automation_record_task from accounts.tasks import execute_automation_record_task
from accounts.utils import account_secret_task_status
from authentication.permissions import UserConfirmation, ConfirmType from authentication.permissions import UserConfirmation, ConfirmType
from common.permissions import IsValidLicense from common.permissions import IsValidLicense
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet, RecordListMixin AutomationNodeAddRemoveApi, AutomationExecutionViewSet
) )
__all__ = [ __all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi', 'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi', 'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
'ChangSecretNodeAddRemoveApi' 'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet'
] ]
@@ -35,7 +38,7 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
serializer_class = serializers.ChangeSecretAutomationSerializer serializer_class = serializers.ChangeSecretAutomationSerializer
class ChangeSecretRecordViewSet(RecordListMixin, mixins.ListModelMixin, OrgGenericViewSet): class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = ChangeSecretRecordFilterSet filterset_class = ChangeSecretRecordFilterSet
permission_classes = [RBACPermission, IsValidLicense] permission_classes = [RBACPermission, IsValidLicense]
search_fields = ('asset__address', 'account__username') search_fields = ('asset__address', 'account__username')
@@ -94,12 +97,13 @@ class ChangeSecretRecordViewSet(RecordListMixin, mixins.ListModelMixin, OrgGener
def execute(self, request, *args, **kwargs): def execute(self, request, *args, **kwargs):
record_ids = request.data.get('record_ids') record_ids = request.data.get('record_ids')
records = self.get_queryset().filter(id__in=record_ids) records = self.get_queryset().filter(id__in=record_ids)
execution_count = records.values_list('execution_id', flat=True).distinct().count() if not records.exists():
if execution_count != 1:
return Response( return Response(
{'detail': 'Only one execution is allowed to execute'}, {'detail': 'No valid records found'},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
record_ids = [str(_id) for _id in records.values_list('id', flat=True)]
task = execute_automation_record_task.delay(record_ids, self.tp) task = execute_automation_record_task.delay(record_ids, self.tp)
return Response({'task': task.id}, status=status.HTTP_200_OK) return Response({'task': task.id}, status=status.HTTP_200_OK)
@@ -154,3 +158,25 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi): class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = ChangeSecretAutomation model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateNodeSerializer serializer_class = serializers.ChangeSecretUpdateNodeSerializer
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
perm_model = ChangeSecretAutomation
filterset_class = ChangeSecretStatusFilterSet
serializer_class = serializers.ChangeSecretAccountSerializer
search_fields = ('username',)
permission_classes = [RBACPermission, IsValidLicense]
http_method_names = ["get", "delete", "options"]
def get_queryset(self):
account_ids = list(account_secret_task_status.account_ids)
return Account.objects.filter(id__in=account_ids).select_related('asset')
def bulk_destroy(self, request, *args, **kwargs):
account_ids = request.data.get('account_ids')
if isinstance(account_ids, str):
account_ids = [account_ids]
for _id in account_ids:
account_secret_task_status.clear(_id)
return Response(status=status.HTTP_200_OK)

View File

@@ -90,10 +90,10 @@ class ChangeSecretDashboardApi(APIView):
def get_change_secret_asset_queryset(self): def get_change_secret_asset_queryset(self):
qs = self.change_secrets_queryset qs = self.change_secrets_queryset
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct() node_ids = qs.values_list('nodes', flat=True).distinct()
nodes = Node.objects.filter(id__in=node_ids) nodes = Node.objects.filter(id__in=node_ids).only('id', 'key')
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct() direct_asset_ids = qs.values_list('assets', flat=True).distinct()
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids)) asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
return Asset.objects.filter(id__in=asset_ids) return Asset.objects.filter(id__in=asset_ids)

View File

@@ -45,10 +45,10 @@ class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
class CheckAccountExecutionViewSet(AutomationExecutionViewSet): class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = ( rbac_perms = (
("list", "accounts.view_checkaccountexecution"), ("list", "accounts.view_checkaccountexecution"),
("retrieve", "accounts.view_checkaccountsexecution"), ("retrieve", "accounts.view_checkaccountexecution"),
("create", "accounts.add_checkaccountexecution"), ("create", "accounts.add_checkaccountexecution"),
("adhoc", "accounts.add_checkaccountexecution"), ("adhoc", "accounts.add_checkaccountexecution"),
("report", "accounts.view_checkaccountsexecution"), ("report", "accounts.view_checkaccountexecution"),
) )
ordering = ("-date_created",) ordering = ("-date_created",)
tp = AutomationTypes.check_account tp = AutomationTypes.check_account
@@ -147,6 +147,7 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
serializer_class = serializers.CheckAccountEngineSerializer serializer_class = serializers.CheckAccountEngineSerializer
permission_classes = [RBACPermission, IsValidLicense] permission_classes = [RBACPermission, IsValidLicense]
perm_model = CheckAccountEngine perm_model = CheckAccountEngine
http_method_names = ['get', 'options']
def get_queryset(self): def get_queryset(self):
return CheckAccountEngine.get_default_engines() return CheckAccountEngine.get_default_engines()

View File

@@ -9,7 +9,7 @@ from accounts.models import PushAccountAutomation, PushSecretRecord
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet, RecordListMixin AutomationNodeAddRemoveApi, AutomationExecutionViewSet
) )
__all__ = [ __all__ = [
@@ -42,7 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
return queryset return queryset
class PushAccountRecordViewSet(RecordListMixin, mixins.ListModelMixin, OrgGenericViewSet): class PushAccountRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = PushAccountRecordFilterSet filterset_class = PushAccountRecordFilterSet
search_fields = ('asset__address', 'account__username') search_fields = ('asset__address', 'account__username')
ordering_fields = ('date_finished',) ordering_fields = ('date_finished',)

View File

@@ -5,12 +5,13 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.automations.methods import platform_automation_methods from accounts.automations.methods import platform_automation_methods
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice, \
ChangeSecretAccountStatus
from accounts.models import BaseAccountQuerySet from accounts.models import BaseAccountQuerySet
from accounts.utils import SecretGenerator from accounts.utils import SecretGenerator, account_secret_task_status
from assets.automations.base.manager import BasePlaybookManager from assets.automations.base.manager import BasePlaybookManager
from assets.const import HostTypes from assets.const import HostTypes
from common.db.utils import safe_db_connection from common.db.utils import safe_atomic_db_connection
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -36,7 +37,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
) )
self.account_ids = self.execution.snapshot['accounts'] self.account_ids = self.execution.snapshot['accounts']
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试 self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
self.name_recorder_mapper = {} # 做个映射,方便后面处理 self.name_record_mapper = {} # 做个映射,方便后面处理
def gen_account_inventory(self, account, asset, h, path_dir): def gen_account_inventory(self, account, asset, h, path_dir):
raise NotImplementedError raise NotImplementedError
@@ -69,7 +70,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
return return
asset = privilege_account.asset asset = privilege_account.asset
accounts = asset.accounts.all() accounts = asset.all_accounts.all()
accounts = accounts.filter(id__in=self.account_ids, secret_reset=True) accounts = accounts.filter(id__in=self.account_ids, secret_reset=True)
if self.secret_type: if self.secret_type:
@@ -94,6 +95,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
h['account'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,
'full_username': account.full_username,
'secret_type': secret_type, 'secret_type': secret_type,
'secret': account.escape_jinja2_syntax(new_secret), 'secret': account.escape_jinja2_syntax(new_secret),
'private_key_path': private_key_path, 'private_key_path': private_key_path,
@@ -111,10 +113,15 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if host.get('error'): if host.get('error'):
return host return host
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
host['ssh_params'] = {} host['ssh_params'] = {}
accounts = self.get_accounts(account) accounts = self.get_accounts(account)
existing_ids = set(map(str, accounts.values_list('id', flat=True)))
missing_ids = set(map(str, self.account_ids)) - existing_ids
for account_id in missing_ids:
self.clear_account_queue_status(account_id)
error_msg = _("No pending accounts found") error_msg = _("No pending accounts found")
if not accounts: if not accounts:
print(f'{asset}: {error_msg}') print(f'{asset}: {error_msg}')
@@ -131,31 +138,50 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts h['name'] += '(' + account.username + ')' # To distinguish different accounts
account_status = account_secret_task_status.get_status(account.id)
if account_status == ChangeSecretAccountStatus.PROCESSING:
h['error'] = f'Account is already being processed, skipping: {account}'
inventory_hosts.append(h)
continue
try: try:
h = self.gen_account_inventory(account, asset, h, path_dir) h, record = self.gen_account_inventory(account, asset, h, path_dir)
h['check_conn_after_change'] = record.execution.snapshot.get('check_conn_after_change', True)
account_secret_task_status.set_status(
account.id,
ChangeSecretAccountStatus.PROCESSING,
metadata={'execution_id': self.execution.id}
)
except Exception as e: except Exception as e:
h['error'] = str(e) h['error'] = str(e)
self.clear_account_queue_status(account.id)
inventory_hosts.append(h) inventory_hosts.append(h)
return inventory_hosts return inventory_hosts
@staticmethod @staticmethod
def save_record(recorder): def save_record(record):
recorder.save(update_fields=['error', 'status', 'date_finished']) record.save(update_fields=['error', 'status', 'date_finished'])
@staticmethod
def clear_account_queue_status(account_id):
account_secret_task_status.clear(account_id)
def on_host_success(self, host, result): def on_host_success(self, host, result):
recorder = self.name_recorder_mapper.get(host) record = self.name_record_mapper.get(host)
if not recorder: if not record:
return return
recorder.status = ChangeSecretRecordStatusChoice.success.value record.status = ChangeSecretRecordStatusChoice.success.value
recorder.date_finished = timezone.now() record.date_finished = timezone.now()
account = recorder.account account = record.account
if not account: if not account:
print("Account not found, deleted ?") print("Account not found, deleted ?")
return return
account.secret = getattr(recorder, 'new_secret', account.secret) account.secret = getattr(record, 'new_secret', account.secret)
account.date_updated = timezone.now() account.date_updated = timezone.now()
account.date_change_secret = timezone.now() account.date_change_secret = timezone.now()
account.change_secret_status = ChangeSecretRecordStatusChoice.success account.change_secret_status = ChangeSecretRecordStatusChoice.success
@@ -169,18 +195,19 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
) )
super().on_host_success(host, result) super().on_host_success(host, result)
with safe_db_connection(): with safe_atomic_db_connection():
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status']) account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
self.save_record(recorder) self.save_record(record)
self.clear_account_queue_status(account.id)
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
recorder = self.name_recorder_mapper.get(host) record = self.name_record_mapper.get(host)
if not recorder: if not record:
return return
recorder.status = ChangeSecretRecordStatusChoice.failed.value record.status = ChangeSecretRecordStatusChoice.failed.value
recorder.date_finished = timezone.now() record.date_finished = timezone.now()
recorder.error = error record.error = error
account = recorder.account account = record.account
if not account: if not account:
print("Account not found, deleted ?") print("Account not found, deleted ?")
return return
@@ -191,12 +218,13 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
self.summary['fail_accounts'] += 1 self.summary['fail_accounts'] += 1
self.result['fail_accounts'].append( self.result['fail_accounts'].append(
{ {
"asset": str(recorder.asset), "asset": str(record.asset),
"username": recorder.account.username, "username": record.account.username,
} }
) )
super().on_host_error(host, error, result) super().on_host_error(host, error, result)
with safe_db_connection(): with safe_atomic_db_connection():
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated']) account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
self.save_record(recorder) self.save_record(record)
self.clear_account_queue_status(account.id)

View File

@@ -53,4 +53,6 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}" ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options: connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
when: check_conn_after_change when: check_conn_after_change
register: result
failed_when: not result.is_available

View File

@@ -39,7 +39,8 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}" priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true ignore_errors: true
when: db_info is succeeded when: db_info is succeeded

View File

@@ -56,3 +56,5 @@
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}" ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
when: check_conn_after_change when: check_conn_after_change
register: result
failed_when: not result.is_available

View File

@@ -41,6 +41,7 @@
password: "{{ account.secret | password_hash('des') }}" password: "{{ account.secret | password_hash('des') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -83,6 +84,7 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -101,7 +103,9 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" and check_conn_after_change when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -112,5 +116,7 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" and check_conn_after_change when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -41,6 +41,7 @@
password: "{{ account.secret | password_hash('sha512') }}" password: "{{ account.secret | password_hash('sha512') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -83,6 +84,7 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -101,7 +103,9 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" and check_conn_after_change when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -112,5 +116,7 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" and check_conn_after_change when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -8,7 +8,7 @@ type:
params: params:
- name: groups - name: groups
type: str type: str
label: '用户组' label: "{{ 'Params groups label' | trans }}"
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
@@ -24,3 +24,7 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -0,0 +1,27 @@
- hosts: demo
gather_facts: no
tasks:
- name: Test privileged account
ansible.windows.win_ping:
- name: Change password
community.windows.win_domain_user:
name: "{{ account.username }}"
password: "{{ account.secret }}"
update_password: always
password_never_expires: yes
state: present
groups: "{{ params.groups }}"
groups_action: add
ignore_errors: true
when: account.secret_type == "password"
- name: Refresh connection
ansible.builtin.meta: reset_connection
- name: Verify password
ansible.windows.win_ping:
vars:
ansible_user: "{{ account.full_username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password" and check_conn_after_change

View File

@@ -0,0 +1,32 @@
id: change_secret_ad_windows
name: "{{ 'Windows account change secret' | trans }}"
version: 1
method: change_secret
category:
- ds
type:
- windows_ad
params:
- name: groups
type: str
label: "{{ 'Params groups label' | trans }}"
default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}"
i18n:
Windows account change secret:
zh: '使用 Ansible 模块 win_domain_user 执行 Windows 账号改密'
ja: 'Ansible win_domain_user モジュールを使用して Windows アカウントのパスワード変更'
en: 'Using Ansible module win_domain_user to change Windows account secret'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -9,19 +9,24 @@ priority: 49
params: params:
- name: groups - name: groups
type: str type: str
label: '用户组' label: "{{ 'Params groups label' | trans }}"
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
i18n: i18n:
Windows account change secret rdp verify: Windows account change secret rdp verify:
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密 RDP 协议测试最后的可连接性' zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密(最后使用 Python 模块 pyfreerdp 验证账号的可连接性'
ja: 'Ansibleモジュールwin_userWindowsアカウントの改密RDPプロトコルテストの最後の接続性を実行する' ja: 'Ansible モジュール win_user を使用して Windows アカウントのパスワードを変更します (最後に Python モジュール pyfreerdp を使用してアカウントの接続を確認します)'
en: 'Using the Ansible module win_user performs Windows account encryption RDP protocol testing for final connectivity' en: 'Use the Ansible module win_user to change the Windows account password (finally use the Python module pyfreerdp to verify the account connectivity)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -30,28 +30,28 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
record = self.get_or_create_record(asset, account, h['name']) record = self.get_or_create_record(asset, account, h['name'])
new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir) new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir)
h = self.gen_inventory(h, account, new_secret, private_key_path, asset) h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
return h return h, record
def get_or_create_record(self, asset, account, name): def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}' asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id in self.record_map: if asset_account_id in self.record_map:
record_id = self.record_map[asset_account_id] record_id = self.record_map[asset_account_id]
recorder = ChangeSecretRecord.objects.filter(id=record_id).first() record = ChangeSecretRecord.objects.filter(id=record_id).first()
else: else:
new_secret = self.get_secret(account) new_secret = self.get_secret(account)
recorder = self.create_record(asset, account, new_secret) record = self.create_record(asset, account, new_secret)
self.name_recorder_mapper[name] = recorder self.name_record_mapper[name] = record
return recorder return record
def create_record(self, asset, account, new_secret): def create_record(self, asset, account, new_secret):
recorder = ChangeSecretRecord( record = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution, asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret, old_secret=account.secret, new_secret=new_secret,
comment=f'{account.username}@{asset.address}' comment=f'{account.username}@{asset.address}'
) )
return recorder return record
def check_secret(self): def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \ if self.secret_strategy == SecretStrategy.custom \
@@ -61,10 +61,10 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
return True return True
@staticmethod @staticmethod
def get_summary(recorders): def get_summary(records):
total, succeed, failed = 0, 0, 0 total, succeed, failed = 0, 0, 0
for recorder in recorders: for record in records:
if recorder.status == ChangeSecretRecordStatusChoice.success.value: if record.status == ChangeSecretRecordStatusChoice.success.value:
succeed += 1 succeed += 1
else: else:
failed += 1 failed += 1
@@ -73,8 +73,8 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
return summary return summary
def print_summary(self): def print_summary(self):
recorders = list(self.name_recorder_mapper.values()) records = list(self.name_record_mapper.values())
summary = self.get_summary(recorders) summary = self.get_summary(records)
print('\n\n' + '-' * 80) print('\n\n' + '-' * 80)
plan_execution_end = _('Plan execution end') plan_execution_end = _('Plan execution end')
print('{} {}\n'.format(plan_execution_end, local_now_filename())) print('{} {}\n'.format(plan_execution_end, local_now_filename()))
@@ -86,7 +86,7 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if self.secret_type and not self.check_secret(): if self.secret_type and not self.check_secret():
return return
recorders = list(self.name_recorder_mapper.values()) records = list(self.name_record_mapper.values())
if self.record_map: if self.record_map:
return return
@@ -98,17 +98,17 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
for user in recipients: for user in recipients:
ChangeSecretReportMsg(user, context).publish() ChangeSecretReportMsg(user, context).publish()
if not recorders: if not records:
return return
summary = self.get_summary(recorders) summary = self.get_summary(records)
self.send_recorder_mail(recipients, recorders, summary) self.send_record_mail(recipients, records, summary)
def send_recorder_mail(self, recipients, recorders, summary): def send_record_mail(self, recipients, records, summary):
name = self.execution.snapshot['name'] name = self.execution.snapshot['name']
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx') filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
if not self.create_file(recorders, filename): if not self.create_file(records, filename):
return return
for user in recipients: for user in recipients:
@@ -121,9 +121,9 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
os.remove(filename) os.remove(filename)
@staticmethod @staticmethod
def create_file(recorders, filename): def create_file(records, filename):
serializer_cls = ChangeSecretRecordBackUpSerializer serializer_cls = ChangeSecretRecordBackUpSerializer
serializer = serializer_cls(recorders, many=True) serializer = serializer_cls(records, many=True)
header = [str(v.label) for v in serializer.child.fields.values()] header = [str(v.label) for v in serializer.child.fields.values()]
rows = [[str(i) for i in row.values()] for row in serializer.data] rows = [[str(i) for i in row.values()] for row in serializer.data]

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2805a0264fc07ae597704841ab060edef8bf74654f525bc778cb9195d8cad0e
size 2547712

View File

@@ -12,13 +12,16 @@ from accounts.models import Account, AccountRisk, RiskChoice
from assets.automations.base.manager import BaseManager from assets.automations.base.manager import BaseManager
from common.const import ConfirmOrIgnore from common.const import ConfirmOrIgnore
from common.decorators import bulk_create_decorator, bulk_update_decorator from common.decorators import bulk_create_decorator, bulk_update_decorator
from settings.models import LeakPasswords
# 已设置手动 finish
@bulk_create_decorator(AccountRisk) @bulk_create_decorator(AccountRisk)
def create_risk(data): def create_risk(data):
return AccountRisk(**data) return AccountRisk(**data)
# 已设置手动 finish
@bulk_update_decorator(AccountRisk, update_fields=["details", "status"]) @bulk_update_decorator(AccountRisk, update_fields=["details", "status"])
def update_risk(risk): def update_risk(risk):
return risk return risk
@@ -157,10 +160,8 @@ class CheckLeakHandler(BaseCheckHandler):
if not account.secret: if not account.secret:
return False return False
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1' is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists()
self.cursor.execute(sql, (account.secret,)) return is_exist
leak = self.cursor.fetchone() is not None
return leak
def clean(self): def clean(self):
self.cursor.close() self.cursor.close()
@@ -218,6 +219,9 @@ class CheckAccountManager(BaseManager):
"details": [{"datetime": now, 'type': 'init'}], "details": [{"datetime": now, 'type': 'init'}],
}) })
create_risk.finish()
update_risk.finish()
def pre_run(self): def pre_run(self):
super().pre_run() super().pre_run()
self.assets = self.execution.get_all_assets() self.assets = self.execution.get_all_assets()
@@ -236,6 +240,11 @@ class CheckAccountManager(BaseManager):
print("Check: {} => {}".format(account, msg)) print("Check: {} => {}".format(account, msg))
if not error: if not error:
AccountRisk.objects.filter(
asset=account.asset,
username=account.username,
risk=handler.risk
).delete()
continue continue
self.add_risk(handler.risk, account) self.add_risk(handler.risk, account)
self.commit_risks(_assets) self.commit_risks(_assets)
@@ -265,7 +274,7 @@ class CheckAccountManager(BaseManager):
handler.clean() handler.clean()
def get_report_subject(self): def get_report_subject(self):
return "Check account report of %s" % self.execution.id return _("Check account report of {}").format(self.execution.id)
def get_report_template(self): def get_report_template(self):
return "accounts/check_account_report.html" return "accounts/check_account_report.html"

View File

@@ -13,6 +13,7 @@ def parse_date(date_str, default=None):
formats = [ formats = [
'%Y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S',
'%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S',
'%Y-%m-%d %H:%M:%S',
'%d-%m-%Y %H:%M:%S', '%d-%m-%Y %H:%M:%S',
'%Y/%m/%d', '%Y/%m/%d',
'%d-%m-%Y', '%d-%m-%Y',
@@ -26,7 +27,6 @@ def parse_date(date_str, default=None):
return default return default
# TODO 后期会挪到 playbook 中
class GatherAccountsFilter: class GatherAccountsFilter:
def __init__(self, tp): def __init__(self, tp):
self.tp = tp self.tp = tp
@@ -208,14 +208,35 @@ class GatherAccountsFilter:
key, value = parts key, value = parts
user_info[key.strip()] = value.strip() user_info[key.strip()] = value.strip()
detail = {'groups': user_info.get('Global Group memberships', ''), } detail = {'groups': user_info.get('Global Group memberships', ''), }
user = {
'username': user_info.get('User name', ''), username = user_info.get('User name')
'date_password_change': parse_date(user_info.get('Password last set', '')), if not username:
'date_password_expired': parse_date(user_info.get('Password expires', '')), continue
'date_last_login': parse_date(user_info.get('Last logon', '')),
result[username] = {
'username': username,
'date_password_change': parse_date(user_info.get('Password last set')),
'date_password_expired': parse_date(user_info.get('Password expires')),
'date_last_login': parse_date(user_info.get('Last logon')),
'groups': detail,
}
return result
@staticmethod
def windows_ad_filter(info):
result = {}
for user_info in info['user_details']:
detail = {'groups': user_info.get('GlobalGroupMemberships', ''), }
username = user_info.get('SamAccountName')
if not username:
continue
result[username] = {
'username': username,
'date_password_change': parse_date(user_info.get('PasswordLastSet')),
'date_password_expired': parse_date(user_info.get('PasswordExpires')),
'date_last_login': parse_date(user_info.get('LastLogonDate')),
'groups': detail, 'groups': detail,
} }
result[user['username']] = user
return result return result
@staticmethod @staticmethod

View File

@@ -4,6 +4,7 @@
- name: Run net user command to get all users - name: Run net user command to get all users
win_shell: net user win_shell: net user
register: user_list_output register: user_list_output
failed_when: false
- name: Parse all users from net user command - name: Parse all users from net user command
set_fact: set_fact:

View File

@@ -2,10 +2,13 @@ id: gather_accounts_windows
name: "{{ 'Windows account gather' | trans }}" name: "{{ 'Windows account gather' | trans }}"
version: 1 version: 1
method: gather_accounts method: gather_accounts
category: host category:
- host
type: type:
- windows - windows
i18n: i18n:
Windows account gather: Windows account gather:
zh: 使用命令 net user 收集 Windows 账号 zh: 使用命令 net user 收集 Windows 账号

View File

@@ -0,0 +1,74 @@
- hosts: demo
gather_facts: no
tasks:
- name: Import ActiveDirectory module
win_shell: Import-Module ActiveDirectory
args:
warn: false
- name: Get the SamAccountName list of all AD users
win_shell: |
Import-Module ActiveDirectory
Get-ADUser -Filter * | Select-Object -ExpandProperty SamAccountName
register: ad_user_list
- name: Set the all_users variable
set_fact:
all_users: "{{ ad_user_list.stdout_lines }}"
- name: Get detailed information for each user
win_shell: |
Import-Module ActiveDirectory
$user = Get-ADUser -Identity {{ item }} -Properties Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet, msDS-UserPasswordExpiryTimeComputed, MemberOf
$globalGroups = @()
if ($user.MemberOf) {
$globalGroups = $user.MemberOf | ForEach-Object {
try {
$group = Get-ADGroup $_ -ErrorAction Stop
if ($group.GroupScope -eq 'Global') { $group.Name }
} catch {
}
}
}
$passwordExpiry = $null
$expiryRaw = $user.'msDS-UserPasswordExpiryTimeComputed'
if ($expiryRaw) {
try {
$passwordExpiry = [datetime]::FromFileTime($expiryRaw)
} catch {
$passwordExpiry = $null
}
}
$output = [PSCustomObject]@{
Name = $user.Name
SamAccountName = $user.SamAccountName
Enabled = $user.Enabled
LastLogonDate = if ($user.LastLogonDate) { $user.LastLogonDate.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
PasswordLastSet = if ($user.PasswordLastSet) { $user.PasswordLastSet.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
PasswordExpires = if ($passwordExpiry) { $passwordExpiry.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
GlobalGroupMemberships = $globalGroups
}
$output | ConvertTo-Json -Depth 3
loop: "{{ all_users }}"
register: ad_user_details
ignore_errors: yes
- set_fact:
info:
user_details: >-
{{
ad_user_details.results
| selectattr('rc', 'equalto', 0)
| map(attribute='stdout')
| select('truthy')
| map('from_json')
}}
- debug:
var: info

View File

@@ -0,0 +1,15 @@
id: gather_accounts_windows_ad
name: "{{ 'Windows account gather' | trans }}"
version: 1
method: gather_accounts
category:
- ds
type:
- windows_ad
i18n:
Windows account gather:
zh: 使用命令 Get-ADUser 收集 Windows 账号
ja: コマンド Get-ADUser を使用して Windows アカウントを収集する
en: Using command Get-ADUser to gather accounts

View File

@@ -1,6 +1,6 @@
import time
from collections import defaultdict from collections import defaultdict
import time
from django.utils import timezone from django.utils import timezone
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
@@ -30,6 +30,16 @@ common_risk_items = [
diff_items = risk_items + common_risk_items diff_items = risk_items + common_risk_items
@bulk_create_decorator(AccountRisk)
def _create_risk(data):
return AccountRisk(**data)
@bulk_update_decorator(AccountRisk, update_fields=["details"])
def _update_risk(account):
return account
def format_datetime(value): def format_datetime(value):
if isinstance(value, timezone.datetime): if isinstance(value, timezone.datetime):
return value.strftime("%Y-%m-%d %H:%M:%S") return value.strftime("%Y-%m-%d %H:%M:%S")
@@ -141,25 +151,17 @@ class AnalyseAccountRisk:
found = assets_risks.get(key) found = assets_risks.get(key)
if not found: if not found:
self._create_risk(dict(**d, details=[detail])) _create_risk(dict(**d, details=[detail]))
continue continue
found.details.append(detail) found.details.append(detail)
self._update_risk(found) _update_risk(found)
@bulk_create_decorator(AccountRisk)
def _create_risk(self, data):
return AccountRisk(**data)
@bulk_update_decorator(AccountRisk, update_fields=["details"])
def _update_risk(self, account):
return account
def lost_accounts(self, asset, lost_users): def lost_accounts(self, asset, lost_users):
if not self.check_risk: if not self.check_risk:
return return
for user in lost_users: for user in lost_users:
self._create_risk( _create_risk(
dict( dict(
asset_id=str(asset.id), asset_id=str(asset.id),
username=user, username=user,
@@ -176,7 +178,7 @@ class AnalyseAccountRisk:
self._analyse_item_changed(ga, d) self._analyse_item_changed(ga, d)
if not sys_found: if not sys_found:
basic = {"asset": asset, "username": d["username"], 'gathered_account': ga} basic = {"asset": asset, "username": d["username"], 'gathered_account': ga}
self._create_risk( _create_risk(
dict( dict(
**basic, **basic,
risk=RiskChoice.new_found, risk=RiskChoice.new_found,
@@ -222,6 +224,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def _collect_asset_account_info(self, asset, info): def _collect_asset_account_info(self, asset, info):
result = self._filter_success_result(asset.type, info) result = self._filter_success_result(asset.type, info)
accounts = [] accounts = []
for username, info in result.items(): for username, info in result.items():
self.asset_usernames_mapper[str(asset.id)].add(username) self.asset_usernames_mapper[str(asset.id)].add(username)
@@ -373,6 +376,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
for asset, accounts_data in self.asset_account_info.items(): for asset, accounts_data in self.asset_account_info.items():
ori_users = self.ori_asset_usernames[str(asset.id)] ori_users = self.ori_asset_usernames[str(asset.id)]
need_analyser_gather_account = []
with tmp_to_org(asset.org_id): with tmp_to_org(asset.org_id):
for d in accounts_data: for d in accounts_data:
username = d["username"] username = d["username"]
@@ -385,10 +389,12 @@ class GatherAccountsManager(AccountBasePlaybookManager):
ga = ori_account ga = ori_account
self.update_gathered_account(ori_account, d) self.update_gathered_account(ori_account, d)
ori_found = username in ori_users ori_found = username in ori_users
risk_analyser.analyse_risk(asset, ga, d, ori_found) need_analyser_gather_account.append((asset, ga, d, ori_found))
# 这里顺序不能调整risk 外键关联了 gathered_account 主键 id所以在创建 risk 需要保证 gathered_account 已经创建完成
self.create_gathered_account.finish() self.create_gathered_account.finish()
self.update_gathered_account.finish() self.update_gathered_account.finish()
for analysis_data in need_analyser_gather_account:
risk_analyser.analyse_risk(*analysis_data)
self.update_gather_accounts_status(asset) self.update_gather_accounts_status(asset)
if not self.is_sync_account: if not self.is_sync_account:
continue continue
@@ -400,6 +406,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
present=True present=True
) )
# 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步 # 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步
_update_risk.finish()
_create_risk.finish()
time.sleep(0.5) time.sleep(0.5)
def get_report_template(self): def get_report_template(self):

View File

@@ -20,10 +20,11 @@
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}" become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
register: ping_info register: ping_info
delegate_to: localhost delegate_to: localhost
- name: Change asset password (paramiko) - name: Push asset password (paramiko)
custom_command: custom_command:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
@@ -39,7 +40,10 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
commands: "{{ params.commands }}" commands: "{{ params.commands }}"
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}" answers: "{{ params.answers }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delay_time: "{{ params.delay_time | default(2) }}"
prompt: "{{ params.prompt | default('.*') }}"
ignore_errors: true ignore_errors: true
when: ping_info is succeeded and check_conn_after_change when: ping_info is succeeded and check_conn_after_change
register: change_info register: change_info
@@ -58,5 +62,6 @@
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delegate_to: localhost delegate_to: localhost
when: check_conn_after_change when: check_conn_after_change

View File

@@ -10,10 +10,30 @@ protocol: ssh
priority: 50 priority: 50
params: params:
- name: commands - name: commands
type: list type: text
label: "{{ 'Params commands label' | trans }}" label: "{{ 'Params commands label' | trans }}"
default: [ '' ] default: ''
help_text: "{{ 'Params commands help text' | trans }}" help_text: "{{ 'Params commands help text' | trans }}"
- name: recv_timeout
type: int
label: "{{ 'Params recv_timeout label' | trans }}"
default: 30
help_text: "{{ 'Params recv_timeout help text' | trans }}"
- name: delay_time
type: int
label: "{{ 'Params delay_time label' | trans }}"
default: 2
help_text: "{{ 'Params delay_time help text' | trans }}"
- name: prompt
type: str
label: "{{ 'Params prompt label' | trans }}"
default: '.*'
help_text: "{{ 'Params prompt help text' | trans }}"
- name: answers
type: text
label: "{{ 'Params answer label' | trans }}"
default: '.*'
help_text: "{{ 'Params answer help text' | trans }}"
i18n: i18n:
SSH account push: SSH account push:
@@ -22,11 +42,91 @@ i18n:
en: 'Custom push using SSH command line' en: 'Custom push using SSH command line'
Params commands help text: Params commands help text:
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 &#123;username&#125;、&#123;password&#125;、&#123;login_password&#125;格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br />4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end' zh: |
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />&#123;ユーザー名&#125;、&#123;パスワード&#125;、&#123;login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.&#123;login_password&#125;<br />3 .ターミナルの設定<br / >4. ユーザー名 &#123;ユーザー名&#125; 権限 0 パスワード &#123;パスワード&#125; <br />5. 終了' 请将命令中的指定位置改成特殊符号 <br />
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use &#123;username&#125;, &#123;password&#125;, &#123;login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br / >4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end' 1. 推送账号 -> {username} <br />
2. 推送密码 -> {password} <br />
3. 登录用户密码 -> {login_password} <br />
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<br />
比如针对 Cisco 主机进行推送,一般需要配置五条命令:<br />
enable <br />
{login_password} <br />
configure terminal <br />
username {username} privilege 0 password {password} <br />
end <br />
ja: |
コマンド内の指定された位置を特殊記号に変更してください。<br />
新しいパスワード(アカウント押す) -> {username} <br />
新しいパスワード(パスワード押す) -> {password} <br />
ログインユーザーパスワード -> {login_password} <br />
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります<br />
enable <br />
{login_password} <br />
configure terminal <br />
username {username} privilege 0 password {password} <br />
end <br />
en: |
Please change the specified positions in the command to special symbols. <br />
Change password account -> {username} <br />
Change password -> {password} <br />
Login user password -> {login_password} <br />
<strong>Multiple commands are separated by new lines,</strong> and when executing tasks, <br />
the system will replace the special symbols with real data. <br />
For example, to push the password for a Cisco device, you generally need to configure five commands: <br />
enable <br />
{login_password} <br />
configure terminal <br />
username {username} privilege 0 password {password} <br />
end <br />
Params commands label: Params commands label:
zh: '自定义命令' zh: '自定义命令'
ja: 'カスタムコマンド' ja: 'カスタムコマンド'
en: 'Custom command' en: 'Custom command'
Params recv_timeout label:
zh: '超时时间'
ja: 'タイムアウト'
en: 'Timeout'
Params recv_timeout help text:
zh: '等待命令结果返回的超时时间(秒)'
ja: 'コマンドの結果を待つタイムアウト時間(秒)'
en: 'The timeout for waiting for the command result to return (Seconds)'
Params delay_time label:
zh: '延迟发送时间'
ja: '遅延送信時間'
en: 'Delayed send time'
Params delay_time help text:
zh: '每条命令延迟发送的时间间隔(秒)'
ja: '各コマンド送信の遅延間隔(秒)'
en: 'Time interval for each command delay in sending (Seconds)'
Params prompt label:
zh: '提示符'
ja: 'ヒント'
en: 'Prompt'
Params prompt help text:
zh: '终端连接后显示的提示符信息(正则表达式)'
ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)'
en: 'Prompt information displayed after terminal connection (Regular expression)'
Params answer label:
zh: '命令结果'
ja: 'コマンド結果'
en: 'Command result'
Params answer help text:
zh: |
根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式)
ja: |
結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。
入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん)
en: |
Decide whether to execute the next command based on the result match.
The input content corresponds line by line with the content
of the `Custom command` above. (Regular expression)

View File

@@ -54,3 +54,5 @@
connection_options: connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
when: check_conn_after_change when: check_conn_after_change
register: result
failed_when: not result.is_available

View File

@@ -39,7 +39,8 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}" priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true ignore_errors: true
when: db_info is succeeded when: db_info is succeeded

View File

@@ -41,6 +41,7 @@
password: "{{ account.secret | password_hash('des') }}" password: "{{ account.secret | password_hash('des') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -83,6 +84,7 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -101,7 +103,9 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" and check_conn_after_change when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -112,6 +116,8 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" and check_conn_after_change when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -41,6 +41,7 @@
password: "{{ account.secret | password_hash('sha512') }}" password: "{{ account.secret | password_hash('sha512') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -83,6 +84,7 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -101,7 +103,9 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" and check_conn_after_change when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -112,6 +116,8 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" and check_conn_after_change when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -8,7 +8,7 @@ type:
params: params:
- name: groups - name: groups
type: str type: str
label: '用户组' label: "{{ 'Params groups label' | trans }}"
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
@@ -22,3 +22,8 @@ i18n:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -0,0 +1,27 @@
- hosts: demo
gather_facts: no
tasks:
- name: Test privileged account
ansible.windows.win_ping:
- name: Push user password
community.windows.win_domain_user:
name: "{{ account.username }}"
password: "{{ account.secret }}"
update_password: always
password_never_expires: yes
state: present
groups: "{{ params.groups }}"
groups_action: add
ignore_errors: true
when: account.secret_type == "password"
- name: Refresh connection
ansible.builtin.meta: reset_connection
- name: Verify password
ansible.windows.win_ping:
vars:
ansible_user: "{{ account.full_username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password" and check_conn_after_change

View File

@@ -0,0 +1,30 @@
id: push_account_ad_windows
name: "{{ 'Windows account push' | trans }}"
version: 1
method: push_account
category:
- ds
type:
- windows_ad
params:
- name: groups
type: str
label: "{{ 'Params groups label' | trans }}"
default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}"
i18n:
Windows account push:
zh: '使用 Ansible 模块 win_domain_user 执行 Windows 账号推送'
ja: 'Ansible win_domain_user モジュールを使用して Windows アカウントをプッシュする'
en: 'Using Ansible module win_domain_user to push account'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -9,7 +9,7 @@ priority: 49
params: params:
- name: groups - name: groups
type: str type: str
label: '用户组' label: "{{ 'Params groups label' | trans }}"
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
@@ -23,3 +23,8 @@ i18n:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -12,7 +12,7 @@ logger = get_logger(__name__)
class PushAccountManager(BaseChangeSecretPushManager): class PushAccountManager(BaseChangeSecretPushManager):
@staticmethod @staticmethod
def require_update_version(account, recorder): def require_update_version(account, record):
account.skip_history_when_saving = True account.skip_history_when_saving = True
return False return False
@@ -31,29 +31,29 @@ class PushAccountManager(BaseChangeSecretPushManager):
secret_type = account.secret_type secret_type = account.secret_type
if not secret: if not secret:
raise ValueError(_('Secret cannot be empty')) raise ValueError(_('Secret cannot be empty'))
self.get_or_create_record(asset, account, h['name']) record = self.get_or_create_record(asset, account, h['name'])
new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir) new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir)
h = self.gen_inventory(h, account, new_secret, private_key_path, asset) h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
return h return h, record
def get_or_create_record(self, asset, account, name): def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}' asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id in self.record_map: if asset_account_id in self.record_map:
record_id = self.record_map[asset_account_id] record_id = self.record_map[asset_account_id]
recorder = PushSecretRecord.objects.filter(id=record_id).first() record = PushSecretRecord.objects.filter(id=record_id).first()
else: else:
recorder = self.create_record(asset, account) record = self.create_record(asset, account)
self.name_recorder_mapper[name] = recorder self.name_record_mapper[name] = record
return recorder return record
def create_record(self, asset, account): def create_record(self, asset, account):
recorder = PushSecretRecord( record = PushSecretRecord(
asset=asset, account=account, execution=self.execution, asset=asset, account=account, execution=self.execution,
comment=f'{account.username}@{asset.address}' comment=f'{account.username}@{asset.address}'
) )
return recorder return record
def print_summary(self): def print_summary(self):
print('\n\n' + '-' * 80) print('\n\n' + '-' * 80)

View File

@@ -11,4 +11,5 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: "{{ jms_asset.spec_info.db_name }}" name: "{{ jms_asset.spec_info.db_name }}"
script: "DROP USER {{ account.username }}" script: "DROP LOGIN {{ account.username }}; select @@version"

View File

@@ -0,0 +1,9 @@
- hosts: windows
gather_facts: no
tasks:
- name: "Remove account"
ansible.windows.win_domain_user:
name: "{{ account.username }}"
state: absent

View File

@@ -0,0 +1,14 @@
id: remove_account_ad_windows
name: "{{ 'Windows account remove' | trans }}"
version: 1
method: remove_account
category:
- ds
type:
- windows_ad
i18n:
Windows account remove:
zh: 使用 Ansible 模块 win_domain_user 删除账号
ja: Ansible モジュール win_domain_user を使用してアカウントを削除する
en: Use the Ansible module win_domain_user to delete an account

View File

@@ -3,13 +3,13 @@
vars: vars:
ansible_shell_type: sh ansible_shell_type: sh
ansible_connection: local ansible_connection: local
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks: tasks:
- name: Verify account (pyfreerdp) - name: Verify account (pyfreerdp)
rdp_ping: rdp_ping:
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}" login_user: "{{ account.full_username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"

View File

@@ -2,8 +2,10 @@ id: verify_account_by_rdp
name: "{{ 'Windows rdp account verify' | trans }}" name: "{{ 'Windows rdp account verify' | trans }}"
category: category:
- host - host
- ds
type: type:
- windows - windows
- windows_ad
method: verify_account method: verify_account
protocol: rdp protocol: rdp
priority: 1 priority: 1

View File

@@ -16,3 +16,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}" ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options: connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}" - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
register: result
failed_when: not result.is_available

View File

@@ -8,6 +8,7 @@
ansible_user: "{{ account.username }}" ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
ansible_ssh_private_key_file: "{{ account.private_key_path }}" ansible_ssh_private_key_file: "{{ account.private_key_path }}"
ansible_timeout: 30
when: not account.become.ansible_become when: not account.become.ansible_become
- name: Verify account connectivity(Switch) - name: Verify account connectivity(Switch)
@@ -20,4 +21,5 @@
ansible_become_method: "{{ account.become.ansible_become_method }}" ansible_become_method: "{{ account.become.ansible_become_method }}"
ansible_become_user: "{{ account.become.ansible_become_user }}" ansible_become_user: "{{ account.become.ansible_become_user }}"
ansible_become_password: "{{ account.become.ansible_become_password }}" ansible_become_password: "{{ account.become.ansible_become_password }}"
ansible_timeout: 30
when: account.become.ansible_become when: account.become.ansible_become

View File

@@ -7,5 +7,6 @@
- name: Verify account - name: Verify account
ansible.windows.win_ping: ansible.windows.win_ping:
vars: vars:
ansible_user: "{{ account.username }}" ansible_user: "{{ account.full_username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
ansible_timeout: 30

View File

@@ -2,9 +2,12 @@ id: verify_account_windows
name: "{{ 'Windows account verify' | trans }}" name: "{{ 'Windows account verify' | trans }}"
version: 1 version: 1
method: verify_account method: verify_account
category: host category:
- host
- ds
type: type:
- windows - windows
- windows_ad
i18n: i18n:
Windows account verify: Windows account verify:

View File

@@ -42,7 +42,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
if host.get('error'): if host.get('error'):
return host return host
accounts = asset.accounts.all() accounts = asset.all_accounts.all()
accounts = self.get_accounts(account, accounts) accounts = self.get_accounts(account, accounts)
inventory_hosts = [] inventory_hosts = []
@@ -64,6 +64,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
h['account'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,
'full_username': account.full_username,
'secret_type': account.secret_type, 'secret_type': account.secret_type,
'secret': account.escape_jinja2_syntax(secret), 'secret': account.escape_jinja2_syntax(secret),
'private_key_path': private_key_path, 'private_key_path': private_key_path,
@@ -84,6 +85,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
account = self.host_account_mapper.get(host) account = self.host_account_mapper.get(host)
try: try:
account.set_connectivity(Connectivity.ERR) error_tp = account.get_err_connectivity(error)
account.set_connectivity(error_tp)
except Exception as e: except Exception as e:
print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n') print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')

View File

@@ -17,7 +17,7 @@ __all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice', 'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
'GatherAccountDetailField' 'GatherAccountDetailField', 'ChangeSecretAccountStatus'
] ]
@@ -117,6 +117,12 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
pending = 'pending', _('Pending') pending = 'pending', _('Pending')
class ChangeSecretAccountStatus(models.TextChoices):
QUEUED = 'queued', _('Queued')
READY = 'ready', _('Ready')
PROCESSING = 'processing', _('Processing')
class GatherAccountDetailField(models.TextChoices): class GatherAccountDetailField(models.TextChoices):
can_login = 'can_login', _('Can login') can_login = 'can_login', _('Can login')
superuser = 'superuser', _('Superuser') superuser = 'superuser', _('Superuser')

View File

@@ -5,7 +5,6 @@ import uuid
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as drf_filters from django_filters import rest_framework as drf_filters
from rest_framework import filters from rest_framework import filters
from rest_framework.compat import coreapi from rest_framework.compat import coreapi
@@ -13,10 +12,26 @@ from rest_framework.compat import coreapi
from assets.models import Node from assets.models import Node
from assets.utils import get_node_from_request from assets.utils import get_node_from_request
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.utils import get_logger
from common.utils.timezone import local_zero_hour, local_now from common.utils.timezone import local_zero_hour, local_now
from .const.automation import ChangeSecretRecordStatusChoice from .const.automation import ChangeSecretRecordStatusChoice
from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \ from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \
AutomationExecution AutomationExecution
from .utils import account_secret_task_status
logger = get_logger(__file__)
class UUIDFilterMixin:
@staticmethod
def filter_uuid(queryset, name, value):
try:
uuid.UUID(value)
except ValueError:
logger.warning(f"Invalid UUID: {value}")
return queryset.none()
return queryset.filter(**{name: value})
class NodeFilterBackend(filters.BaseFilterBackend): class NodeFilterBackend(filters.BaseFilterBackend):
@@ -43,14 +58,15 @@ class NodeFilterBackend(filters.BaseFilterBackend):
return queryset return queryset
class AccountFilterSet(BaseFilterSet): class AccountFilterSet(UUIDFilterMixin, BaseFilterSet):
ip = drf_filters.CharFilter(field_name="address", lookup_expr="exact") ip = drf_filters.CharFilter(field_name="address", lookup_expr="exact")
name = drf_filters.CharFilter(field_name="name", lookup_expr="exact")
hostname = drf_filters.CharFilter(field_name="name", lookup_expr="exact") hostname = drf_filters.CharFilter(field_name="name", lookup_expr="exact")
username = drf_filters.CharFilter(field_name="username", lookup_expr="exact") username = drf_filters.CharFilter(field_name="username", lookup_expr="exact")
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr="exact") address = drf_filters.CharFilter(field_name="asset__address", lookup_expr="exact")
asset_id = drf_filters.CharFilter(field_name="asset", lookup_expr="exact") asset_name = drf_filters.CharFilter(field_name="asset__name", lookup_expr="exact")
asset = drf_filters.CharFilter(field_name="asset", lookup_expr="exact") asset_id = drf_filters.CharFilter(field_name="asset", method="filter_uuid")
assets = drf_filters.CharFilter(field_name="asset_id", lookup_expr="exact") assets = drf_filters.CharFilter(field_name="asset_id", method="filter_uuid")
has_secret = drf_filters.BooleanFilter(method="filter_has_secret") has_secret = drf_filters.BooleanFilter(method="filter_has_secret")
platform = drf_filters.CharFilter( platform = drf_filters.CharFilter(
field_name="asset__platform_id", lookup_expr="exact" field_name="asset__platform_id", lookup_expr="exact"
@@ -135,8 +151,9 @@ class AccountFilterSet(BaseFilterSet):
kwargs.update({"date_change_secret__gt": date}) kwargs.update({"date_change_secret__gt": date})
if name == "latest_secret_change_failed": if name == "latest_secret_change_failed":
queryset = queryset.filter(date_change_secret__gt=date).exclude( queryset = (
change_secret_status=ChangeSecretRecordStatusChoice.success queryset.filter(date_change_secret__gt=date)
.exclude(change_secret_status=ChangeSecretRecordStatusChoice.success)
) )
if kwargs: if kwargs:
@@ -146,8 +163,8 @@ class AccountFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
"id", "asset", "source_id", "secret_type", "category", "id", "source_id", "secret_type", "category", "type",
"type", "privileged", "secret_reset", "connectivity", 'is_active' "privileged", "secret_reset", "connectivity", "is_active"
] ]
@@ -185,16 +202,6 @@ class SecretRecordMixin(drf_filters.FilterSet):
return queryset.filter(date_finished__gte=dt) return queryset.filter(date_finished__gte=dt)
class UUIDExecutionFilterMixin:
@staticmethod
def filter_execution(queryset, name, value):
try:
uuid.UUID(value)
except ValueError:
raise ValueError(_('Enter a valid UUID.'))
return queryset.filter(**{name: value})
class DaysExecutionFilterMixin: class DaysExecutionFilterMixin:
days = drf_filters.NumberFilter(method="filter_days") days = drf_filters.NumberFilter(method="filter_days")
field: str field: str
@@ -209,10 +216,10 @@ class DaysExecutionFilterMixin:
class ChangeSecretRecordFilterSet( class ChangeSecretRecordFilterSet(
SecretRecordMixin, UUIDExecutionFilterMixin, SecretRecordMixin, UUIDFilterMixin,
DaysExecutionFilterMixin, BaseFilterSet DaysExecutionFilterMixin, BaseFilterSet
): ):
execution_id = django_filters.CharFilter(method="filter_execution") execution_id = django_filters.CharFilter(method="filter_uuid")
days = drf_filters.NumberFilter(method="filter_days") days = drf_filters.NumberFilter(method="filter_days")
field = 'date_finished' field = 'date_finished'
@@ -227,12 +234,34 @@ class AutomationExecutionFilterSet(DaysExecutionFilterMixin, BaseFilterSet):
class Meta: class Meta:
model = AutomationExecution model = AutomationExecution
fields = ["days", 'trigger', 'automation_id', 'automation__name'] fields = ["days", 'trigger', 'automation__name']
class PushAccountRecordFilterSet(SecretRecordMixin, UUIDExecutionFilterMixin, BaseFilterSet): class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet):
execution_id = django_filters.CharFilter(method="filter_execution") execution_id = django_filters.CharFilter(method="filter_uuid")
class Meta: class Meta:
model = PushSecretRecord model = PushSecretRecord
fields = ["id", "status", "asset_id", "execution_id"] fields = ["id", "status", "asset_id", "execution_id"]
class ChangeSecretStatusFilterSet(BaseFilterSet):
asset_name = drf_filters.CharFilter(
field_name="asset__name", lookup_expr="icontains"
)
status = drf_filters.CharFilter(method='filter_dynamic')
execution_id = drf_filters.CharFilter(method='filter_dynamic')
class Meta:
model = Account
fields = ["username"]
@staticmethod
def filter_dynamic(queryset, name, value):
_ids = list(queryset.values_list('id', flat=True))
data_map = {
_id: account_secret_task_status.get(str(_id)).get(name)
for _id in _ids
}
matched = [_id for _id, v in data_map.items() if v == value]
return queryset.filter(id__in=matched)

View File

@@ -46,11 +46,16 @@ class Migration(migrations.Migration):
], ],
options={ options={
'verbose_name': 'Account', 'verbose_name': 'Account',
'permissions': [('view_accountsecret', 'Can view asset account secret'), 'permissions': [
('view_historyaccount', 'Can view asset history account'), ('view_accountsecret', 'Can view asset account secret'),
('view_historyaccountsecret', 'Can view asset history account secret'), ('view_historyaccount', 'Can view asset history account'),
('verify_account', 'Can verify account'), ('push_account', 'Can push account'), ('view_historyaccountsecret', 'Can view asset history account secret'),
('remove_account', 'Can remove account')], ('verify_account', 'Can verify account'),
('push_account', 'Can push account'),
('remove_account', 'Can remove account'),
('view_accountsession', 'Can view session'),
('view_accountactivity', 'Can view activity')
],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(

View File

@@ -335,6 +335,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
"abstract": False, "abstract": False,
"verbose_name": "Check engine",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -629,10 +630,15 @@ class Migration(migrations.Migration):
name="connectivity", name="connectivity",
field=models.CharField( field=models.CharField(
choices=[ choices=[
("-", "Unknown"), ('-', 'Unknown'),
("na", "N/A"), ('na', 'N/A'),
("ok", "OK"), ('ok', 'OK'),
("err", "Error"), ('err', 'Error'),
('auth_err', 'Authentication error'),
('password_err', 'Invalid password error'),
('openssh_key_err', 'OpenSSH key error'),
('ntlm_err', 'NTLM credentials rejected error'),
('create_temp_err', 'Create temporary error')
], ],
default="-", default="-",
max_length=16, max_length=16,

View File

@@ -0,0 +1,29 @@
# Generated by Django 4.1.13 on 2025-05-06 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_alter_accountrisk_username_and_more'),
]
operations = [
migrations.AlterField(
model_name='account',
name='connectivity',
field=models.CharField(choices=[
('-', 'Unknown'),
('na', 'N/A'),
('ok', 'OK'),
('err', 'Error'),
('rdp_err', 'RDP error'),
('auth_err', 'Authentication error'),
('password_err', 'Invalid password error'),
('openssh_key_err', 'OpenSSH key error'),
('ntlm_err', 'NTLM credentials rejected error'),
('create_temp_err', 'Create temporary error')
],
default='-', max_length=16, verbose_name='Connectivity'),
),
]

View File

@@ -1,65 +1,15 @@
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from django.db.models import Model
from django.utils import translation from django.utils import translation
from django.utils.translation import gettext_noop
from audits.const import ActionChoices from audits.const import ActionChoices
from common.views.mixins import RecordViewLogMixin from audits.handler import create_or_update_operate_log
from common.utils import i18n_fmt
class AccountRecordViewLogMixin(RecordViewLogMixin): class AccountRecordViewLogMixin(object):
get_object: callable get_object: callable
get_queryset: callable model: Model
@staticmethod
def _filter_params(params):
new_params = {}
need_pop_params = ('format', 'order')
for key, value in params.items():
if key in need_pop_params:
continue
if isinstance(value, list):
value = list(filter(None, value))
if value:
new_params[key] = value
return new_params
def get_resource_display(self, request):
query_params = dict(request.query_params)
params = self._filter_params(query_params)
spm_filter = params.pop("spm", None)
if not params and not spm_filter:
display_message = gettext_noop("Export all")
elif spm_filter:
display_message = gettext_noop("Export only selected items")
else:
query = ",".join(
["%s=%s" % (key, value) for key, value in params.items()]
)
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
return display_message
@property
def detail_msg(self):
return i18n_fmt(
gettext_noop('User %s view/export secret'), self.request.user
)
def list(self, request, *args, **kwargs):
list_func = getattr(super(), 'list')
if not callable(list_func):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
response = list_func(request, *args, **kwargs)
with translation.override('en'):
resource_display = self.get_resource_display(request)
ids = [q.id for q in self.get_queryset()]
self.record_logs(
ids, ActionChoices.view, self.detail_msg, resource_display=resource_display
)
return response
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
retrieve_func = getattr(super(), 'retrieve') retrieve_func = getattr(super(), 'retrieve')
@@ -67,9 +17,9 @@ class AccountRecordViewLogMixin(RecordViewLogMixin):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
response = retrieve_func(request, *args, **kwargs) response = retrieve_func(request, *args, **kwargs)
with translation.override('en'): with translation.override('en'):
resource = self.get_object() create_or_update_operate_log(
self.record_logs( ActionChoices.view, self.model._meta.verbose_name,
[resource.id], ActionChoices.view, self.detail_msg, resource=resource force=True, resource=self.get_object(),
) )
return response return response

View File

@@ -116,6 +116,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
('verify_account', _('Can verify account')), ('verify_account', _('Can verify account')),
('push_account', _('Can push account')), ('push_account', _('Can push account')),
('remove_account', _('Can remove account')), ('remove_account', _('Can remove account')),
('view_accountsession', _('Can view session')),
('view_accountactivity', _('Can view activity')),
] ]
def __str__(self): def __str__(self):
@@ -131,9 +133,49 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
@lazyproperty @lazyproperty
def alias(self): def alias(self):
"""
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
"""
if self.username.startswith('@'): if self.username.startswith('@'):
return self.username return self.username
return self.name return str(self.id)
def is_virtual(self):
"""
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
"""
return self.alias.startswith('@')
def is_ds_account(self):
if self.is_virtual():
return ''
if not self.asset.is_directory_service:
return False
return True
@lazyproperty
def ds(self):
if not self.is_ds_account():
return None
return self.asset.ds
@lazyproperty
def ds_domain(self):
"""这个不能去掉perm_account 会动态设置这个值,以更改 full_username"""
if self.is_virtual():
return ''
if self.ds and self.ds.domain_name:
return self.ds.domain_name
return ''
def username_has_domain(self):
return '@' in self.username or '\\' in self.username
@property
def full_username(self):
if not self.username_has_domain() and self.ds_domain:
return '{}@{}'.format(self.username, self.ds_domain)
return self.username
@lazyproperty @lazyproperty
def has_secret(self): def has_secret(self):

View File

@@ -68,8 +68,10 @@ class AccountRisk(JMSOrgBaseModel):
related_name='risks', null=True related_name='risks', null=True
) )
risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices) risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices)
status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending, status = models.CharField(
blank=True, verbose_name=_('Status')) max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending,
blank=True, verbose_name=_('Status')
)
details = models.JSONField(default=list, verbose_name=_('Detail')) details = models.JSONField(default=list, verbose_name=_('Detail'))
class Meta: class Meta:
@@ -119,6 +121,9 @@ class CheckAccountEngine(JMSBaseModel):
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
verbose_name = _('Check engine')
@staticmethod @staticmethod
def get_default_engines(): def get_default_engines():
data = [ data = [
@@ -128,7 +133,7 @@ class CheckAccountEngine(JMSBaseModel):
"name": _("Check the discovered accounts"), "name": _("Check the discovered accounts"),
"comment": _( "comment": _(
"Perform checks and analyses based on automatically discovered account results, " "Perform checks and analyses based on automatically discovered account results, "
"including user groups, public keys, sudoers, and other information" "including user groups, public keys, sudoers, and other information."
) )
}, },
{ {
@@ -144,13 +149,13 @@ class CheckAccountEngine(JMSBaseModel):
"id": "00000000-0000-0000-0000-000000000003", "id": "00000000-0000-0000-0000-000000000003",
"slug": "check_account_repeat", "slug": "check_account_repeat",
"name": _("Check if the account and password are repeated"), "name": _("Check if the account and password are repeated"),
"comment": _("Check if the account is the same as other accounts") "comment": _("Check if the account is the same as other accounts.")
}, },
{ {
"id": "00000000-0000-0000-0000-000000000004", "id": "00000000-0000-0000-0000-000000000004",
"slug": "check_account_leak", "slug": "check_account_leak",
"name": _("Check whether the account password is a common password"), "name": _("Check whether the account password is a common password"),
"comment": _("Check whether the account password is a commonly leaked password") "comment": _("Check whether the account password is a commonly leaked password.")
}, },
] ]
return data return data

View File

@@ -92,8 +92,9 @@ class VirtualAccount(JMSOrgBaseModel):
from .account import Account from .account import Account
username = user.username username = user.username
alias = AliasAccount.USER.value
with tmp_to_org(asset.org): with tmp_to_org(asset.org):
same_account = cls.objects.filter(alias='@USER').first() same_account = cls.objects.filter(alias=alias).first()
secret = '' secret = ''
if same_account and same_account.secret_from_login: if same_account and same_account.secret_from_login:
@@ -101,4 +102,6 @@ class VirtualAccount(JMSOrgBaseModel):
if not secret and not from_permed: if not secret and not from_permed:
secret = input_secret secret = input_secret
return Account(name=AliasAccount.USER.label, username=username, secret=secret) account = Account(name=AliasAccount.USER.label, username=username, secret=secret)
account.alias = alias
return account

View File

@@ -6,6 +6,7 @@ from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storag
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from terminal.models.component.storage import ReplayStorage from terminal.models.component.storage import ReplayStorage
from users.models import User from users.models import User
from users.utils import activate_user_language
class AccountBackupExecutionTaskMsg: class AccountBackupExecutionTaskMsg:
@@ -28,9 +29,10 @@ class AccountBackupExecutionTaskMsg:
).format(name) ).format(name)
def publish(self, attachment_list=None): def publish(self, attachment_list=None):
send_mail_attachment_async( with activate_user_language(self.user):
self.subject, self.message, [self.user.email], attachment_list send_mail_attachment_async(
) self.subject, self.message, [self.user.email], attachment_list
)
class AccountBackupByObjStorageExecutionTaskMsg: class AccountBackupByObjStorageExecutionTaskMsg:
@@ -74,9 +76,10 @@ class ChangeSecretExecutionTaskMsg:
return self.summary + '\n' + default_message return self.summary + '\n' + default_message
def publish(self, attachments=None): def publish(self, attachments=None):
send_mail_attachment_async( with activate_user_language(self.user):
self.subject, self.message, [self.user.email], attachments send_mail_attachment_async(
) self.subject, self.message, [self.user.email], attachments
)
class GatherAccountChangeMsg(UserMessage): class GatherAccountChangeMsg(UserMessage):

View File

@@ -23,7 +23,7 @@ TYPE_CHOICES = [
("delete_both", _("Delete remote")), ("delete_both", _("Delete remote")),
("add_account", _("Add account")), ("add_account", _("Add account")),
("change_password_add", _("Change password and Add")), ("change_password_add", _("Change password and Add")),
("change_password", _("Change password")), ("change_password", _("Change secret")),
] ]

View File

@@ -233,6 +233,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
required=False, queryset=Account.objects, allow_null=True, allow_empty=True, required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
label=_('Su from'), attrs=('id', 'name', 'username') label=_('Su from'), attrs=('id', 'name', 'username')
) )
ds = ObjectRelatedField(read_only=True, label=_('Directory service'), attrs=('id', 'name', 'domain_name'))
class Meta(BaseAccountSerializer.Meta): class Meta(BaseAccountSerializer.Meta):
model = Account model = Account
@@ -241,10 +242,11 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'date_change_secret', 'change_secret_status' 'date_change_secret', 'change_secret_status'
] ]
fields = BaseAccountSerializer.Meta.fields + [ fields = BaseAccountSerializer.Meta.fields + [
'su_from', 'asset', 'version', 'su_from', 'asset', 'version', 'ds',
'source', 'source_id', 'secret_reset', 'source', 'source_id', 'secret_reset',
] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields ] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields
fields = [f for f in fields if f not in ['spec_info']]
extra_kwargs = { extra_kwargs = {
**BaseAccountSerializer.Meta.extra_kwargs, **BaseAccountSerializer.Meta.extra_kwargs,
'name': {'required': False}, 'name': {'required': False},
@@ -258,7 +260,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(
'asset', 'asset__platform', 'asset', 'asset__platform',
'asset__platform__automation' 'asset__platform__automation'
).prefetch_related('labels', 'labels__label') )
return queryset return queryset
@@ -267,7 +269,7 @@ class AccountDetailSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
model = Account model = Account
fields = AccountSerializer.Meta.fields + ['has_secret'] fields = AccountSerializer.Meta.fields + ['has_secret', 'spec_info']
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret'] read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']

View File

@@ -75,7 +75,7 @@ class BaseAccountSerializer(
fields_mini = ["id", "name", "username"] fields_mini = ["id", "name", "username"]
fields_small = fields_mini + [ fields_small = fields_mini + [
"secret_type", "secret", "passphrase", "secret_type", "secret", "passphrase",
"privileged", "is_active", "spec_info", "privileged", "is_active",
] ]
fields_other = ["created_by", "date_created", "date_updated", "comment"] fields_other = ["created_by", "date_created", "date_updated", "comment"]
fields = fields_small + fields_other + ["labels"] fields = fields_small + fields_other + ["labels"]

View File

@@ -1,9 +1,11 @@
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.models import IntegrationApplication from accounts.models import IntegrationApplication
from acls.serializers.rules import ip_group_child_validator, ip_group_help_text from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
from common.serializers.fields import JSONManyToManyField from common.serializers.fields import JSONManyToManyField
from common.utils import random_string
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -27,13 +29,18 @@ class IntegrationApplicationSerializer(BulkOrgResourceModelSerializer):
'name': {'label': _('Name')}, 'name': {'label': _('Name')},
'accounts_amount': {'label': _('Accounts amount')}, 'accounts_amount': {'label': _('Accounts amount')},
'is_active': {'default': True}, 'is_active': {'default': True},
'logo': {'required': False},
} }
def __init__(self, *args, **kwargs): def to_representation(self, instance):
super().__init__(*args, **kwargs) data = super().to_representation(instance)
request_method = self.context.get('request').method if not data.get('logo'):
if request_method == 'PUT': data['logo'] = static('img/logo.png')
self.fields['logo'].required = False return data
def validate(self, attrs):
attrs['secret'] = random_string(36)
return attrs
class IntegrationAccountSecretSerializer(serializers.Serializer): class IntegrationAccountSecretSerializer(serializers.Serializer):

View File

@@ -57,11 +57,15 @@ class AccountTemplateSerializer(BaseAccountSerializer):
fields_unimport_template = ['push_params'] fields_unimport_template = ['push_params']
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer): class AccountDetailTemplateSerializer(AccountTemplateSerializer):
class Meta(AccountTemplateSerializer.Meta): class Meta(AccountTemplateSerializer.Meta):
fields = AccountTemplateSerializer.Meta.fields + ['spec_info'] fields = AccountTemplateSerializer.Meta.fields + ['spec_info']
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountDetailTemplateSerializer):
class Meta(AccountDetailTemplateSerializer.Meta):
fields = AccountDetailTemplateSerializer.Meta.fields
extra_kwargs = { extra_kwargs = {
**AccountTemplateSerializer.Meta.extra_kwargs, **AccountDetailTemplateSerializer.Meta.extra_kwargs,
'secret': {'write_only': False}, 'secret': {'write_only': False},
'spec_info': {'label': _('Spec info')},
} }

View File

@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes from accounts.const import AutomationTypes, AccountBackupType
from accounts.models import BackupAccountAutomation from accounts.models import BackupAccountAutomation
from common.serializers.fields import EncryptedField from common.serializers.fields import EncryptedField
from common.utils import get_logger from common.utils import get_logger
@@ -41,6 +42,17 @@ class BackupAccountSerializer(BaseAutomationSerializer):
'types': {'label': _('Asset type')} 'types': {'label': _('Asset type')}
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_backup_type_choices()
def set_backup_type_choices(self):
field_backup_type = self.fields.get("backup_type")
if not field_backup_type:
return
if not settings.XPACK_LICENSE_IS_VALID:
field_backup_type._choices.pop(AccountBackupType.object_storage, None)
@property @property
def model_type(self): def model_type(self):
return AutomationTypes.backup_account return AutomationTypes.backup_account

View File

@@ -16,6 +16,7 @@ from assets.models import Asset
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAutomationSerializer from .base import BaseAutomationSerializer
from ...utils import account_secret_task_status
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -26,6 +27,7 @@ __all__ = [
'ChangeSecretRecordBackUpSerializer', 'ChangeSecretRecordBackUpSerializer',
'ChangeSecretUpdateAssetSerializer', 'ChangeSecretUpdateAssetSerializer',
'ChangeSecretUpdateNodeSerializer', 'ChangeSecretUpdateNodeSerializer',
'ChangeSecretAccountSerializer'
] ]
@@ -179,3 +181,24 @@ class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ChangeSecretAutomation model = ChangeSecretAutomation
fields = ['id', 'nodes'] fields = ['id', 'nodes']
class ChangeSecretAccountSerializer(serializers.ModelSerializer):
asset = ObjectRelatedField(
queryset=Asset.objects.all(), required=False, label=_("Asset")
)
ttl = serializers.SerializerMethodField(label=_('TTL'))
meta = serializers.SerializerMethodField(label=_('Meta'))
class Meta:
model = Account
fields = ['id', 'username', 'asset', 'meta', 'ttl']
read_only_fields = fields
@staticmethod
def get_meta(obj):
return account_secret_task_status.get(str(obj.id))
@staticmethod
def get_ttl(obj):
return account_secret_task_status.get_ttl(str(obj.id))

View File

@@ -28,7 +28,7 @@ class DiscoverAccountAutomationSerializer(BaseAutomationSerializer):
+ read_only_fields) + read_only_fields)
extra_kwargs = { extra_kwargs = {
'check_risk': { 'check_risk': {
'help_text': _('Whether to check the risk of the gathered accounts.'), 'help_text': _('Whether to check the risk of the discovered accounts.'),
}, },
**BaseAutomationSerializer.Meta.extra_kwargs **BaseAutomationSerializer.Meta.extra_kwargs
} }

View File

@@ -1,4 +1,5 @@
import datetime import datetime
from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.db.models import Q from django.db.models import Q
@@ -72,24 +73,43 @@ def execute_automation_record_task(record_ids, tp):
task_name = gettext_noop('Execute automation record') task_name = gettext_noop('Execute automation record')
with tmp_to_root_org(): with tmp_to_root_org():
records = ChangeSecretRecord.objects.filter(id__in=record_ids) records = ChangeSecretRecord.objects.filter(id__in=record_ids).order_by('-date_updated')
if not records: if not records:
logger.error('No automation record found: {}'.format(record_ids)) logger.error(f'No automation record found: {record_ids}')
return return
record = records[0] seen_accounts = set()
record_map = {f'{record.asset_id}-{record.account_id}': str(record.id) for record in records} unique_records = []
task_snapshot = { for rec in records:
'params': {}, acct = str(rec.account_id)
'record_map': record_map, if acct not in seen_accounts:
'secret': record.new_secret, seen_accounts.add(acct)
'secret_type': record.execution.snapshot.get('secret_type'), unique_records.append(rec)
'assets': [str(instance.asset_id) for instance in records],
'accounts': [str(instance.account_id) for instance in records], exec_groups = defaultdict(list)
} for rec in unique_records:
with tmp_to_org(record.execution.org_id): exec_groups[rec.execution_id].append(rec)
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
for __, group in exec_groups.items():
latest_rec = group[0]
snapshot = getattr(latest_rec.execution, 'snapshot', {}) or {}
record_map = {f"{r.asset_id}-{r.account_id}": str(r.id) for r in group}
assets = [str(r.asset_id) for r in group]
accounts = [str(r.account_id) for r in group]
task_snapshot = {
'params': {},
'record_map': record_map,
'secret': latest_rec.new_secret,
'secret_type': snapshot.get('secret_type'),
'assets': assets,
'accounts': accounts,
}
with tmp_to_org(latest_rec.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
@shared_task( @shared_task(
@@ -107,16 +127,18 @@ def execute_automation_record_task(record_ids, tp):
) )
@register_as_period_task(crontab=CRONTAB_AT_AM_THREE) @register_as_period_task(crontab=CRONTAB_AT_AM_THREE)
def clean_change_secret_and_push_record_period(): def clean_change_secret_and_push_record_period():
from accounts.models import ChangeSecretRecord from accounts.models import ChangeSecretRecord, PushSecretRecord
print('Start clean change secret and push record period') print('Start clean change secret and push record period')
with tmp_to_root_org(): with tmp_to_root_org():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS') days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days) expired_time = now - datetime.timedelta(days=days)
records = ChangeSecretRecord.objects.filter(
date_updated__lt=expired_day
).filter(
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
)
records.delete() null_related_q = Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
expired_q = Q(date_updated__lt=expired_time)
ChangeSecretRecord.objects.filter(null_related_q).delete()
ChangeSecretRecord.objects.filter(expired_q).delete()
PushSecretRecord.objects.filter(null_related_q).delete()
PushSecretRecord.objects.filter(expired_q).delete()

View File

@@ -1,37 +1,107 @@
from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_noop, gettext_lazy as _ from django.utils.translation import gettext_noop, gettext_lazy as _
from accounts.const import AutomationTypes from accounts.const import AutomationTypes, ChangeSecretAccountStatus
from accounts.tasks.common import quickstart_automation_by_snapshot from accounts.tasks.common import quickstart_automation_by_snapshot
from accounts.utils import account_secret_task_status
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import tmp_to_org
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'push_accounts_to_assets_task', 'push_accounts_to_assets_task', 'change_secret_accounts_to_assets_task'
] ]
def _process_accounts(account_ids, automation_model, default_name, automation_type, snapshot=None):
from accounts.models import Account
accounts = Account.objects.filter(id__in=account_ids)
if not accounts:
logger.warning(
"No accounts found for automation task %s with ids %s",
automation_type, account_ids
)
return
task_name = automation_model.generate_unique_name(gettext_noop(default_name))
snapshot = snapshot or {}
snapshot.update({
'accounts': [str(a.id) for a in accounts],
'assets': [str(a.asset_id) for a in accounts],
})
quickstart_automation_by_snapshot(task_name, automation_type, snapshot)
@shared_task( @shared_task(
queue="ansible", queue="ansible",
verbose_name=_('Push accounts to assets'), verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None), activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_( description=_(
"When creating or modifying an account requires account push, this task is executed" "Whenever an account is created or modified and needs pushing to assets, run this task"
) )
) )
def push_accounts_to_assets_task(account_ids, params=None): def push_accounts_to_assets_task(account_ids, params=None):
from accounts.models import PushAccountAutomation from accounts.models import PushAccountAutomation
from accounts.models import Account snapshot = {
accounts = Account.objects.filter(id__in=account_ids)
task_name = gettext_noop("Push accounts to assets")
task_name = PushAccountAutomation.generate_unique_name(task_name)
task_snapshot = {
'accounts': [str(account.id) for account in accounts],
'assets': [str(account.asset_id) for account in accounts],
'params': params or {}, 'params': params or {},
} }
_process_accounts(
account_ids,
PushAccountAutomation,
_("Push accounts to assets"),
AutomationTypes.push_account,
snapshot=snapshot
)
tp = AutomationTypes.push_account
quickstart_automation_by_snapshot(task_name, tp, task_snapshot) @shared_task(
queue="ansible",
verbose_name=_('Change secret accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_(
"When a secret on an account changes and needs pushing to assets, run this task"
)
)
def change_secret_accounts_to_assets_task(account_ids, params=None, snapshot=None, trigger='manual'):
from accounts.models import ChangeSecretAutomation, Account
manager = account_secret_task_status
if trigger == 'delay':
for _id in manager.account_ids:
status = manager.get_status(_id)
# Check if the account is in QUEUED status
if status == ChangeSecretAccountStatus.QUEUED:
account_ids.append(_id)
manager.set_status(_id, ChangeSecretAccountStatus.READY)
if not account_ids:
return
accounts = Account.objects.filter(id__in=account_ids)
if not accounts:
logger.warning(
"No accounts found for change secret automation task with ids %s",
account_ids
)
return
grouped_ids = defaultdict(lambda: defaultdict(list))
for account in accounts:
grouped_ids[account.org_id][account.secret_type].append(str(account.id))
snapshot = snapshot or {}
for org_id, secret_map in grouped_ids.items():
with tmp_to_org(org_id):
for secret_type, ids in secret_map.items():
snapshot['secret_type'] = secret_type
_process_accounts(
ids,
ChangeSecretAutomation,
_("Change secret accounts to assets"),
AutomationTypes.change_secret,
snapshot=snapshot
)

View File

@@ -129,7 +129,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p class="no-data">{% trans 'No new accounts found' %}</p> <p class="no-data">{% trans 'No lost accounts found' %}</p>
{% endif %} {% endif %}
</div> </div>
</section> </section>

View File

@@ -17,6 +17,7 @@ router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet,
router.register(r'account-backup-plans', api.BackupAccountViewSet, 'account-backup') router.register(r'account-backup-plans', api.BackupAccountViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.BackupAccountExecutionViewSet, 'account-backup-execution') router.register(r'account-backup-plan-executions', api.BackupAccountExecutionViewSet, 'account-backup-execution')
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation') router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
router.register(r'change-secret-status', api.ChangeSecretStatusViewSet, 'change-secret-status')
router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution')
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
router.register(r'gather-account-automations', api.DiscoverAccountsAutomationViewSet, 'gather-account-automation') router.register(r'gather-account-automations', api.DiscoverAccountsAutomationViewSet, 'gather-account-automation')

View File

@@ -1,10 +1,11 @@
import copy import copy
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
from common.utils import ssh_key_gen, random_string from common.utils import ssh_key_gen, random_string
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
@@ -61,3 +62,80 @@ def validate_ssh_key(ssh_key, passphrase=None):
if not valid: if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error")) raise serializers.ValidationError(_("private key invalid or passphrase error"))
return parse_ssh_private_key_str(ssh_key, passphrase) return parse_ssh_private_key_str(ssh_key, passphrase)
class AccountSecretTaskStatus:
def __init__(
self,
prefix='queue:change_secret:',
debounce_key='debounce:change_secret:task',
debounce_timeout=10,
queue_status_timeout=60,
default_timeout=3600,
delayed_task_countdown=20,
):
self.prefix = prefix
self.debounce_key = debounce_key
self.debounce_timeout = debounce_timeout
self.queue_status_timeout = queue_status_timeout
self.default_timeout = default_timeout
self.delayed_task_countdown = delayed_task_countdown
self.enabled = getattr(settings, 'CHANGE_SECRET_AFTER_SESSION_END', False)
def _key(self, identifier):
return f"{self.prefix}{identifier}"
@property
def account_ids(self):
for key in cache.iter_keys(f"{self.prefix}*"):
yield key.split(':')[-1]
def is_debounced(self):
return cache.add(self.debounce_key, True, self.debounce_timeout)
def get_queue_key(self, identifier):
return self._key(identifier)
def set_status(
self,
identifier,
status,
timeout=None,
metadata=None,
use_add=False
):
if not self.enabled:
return
key = self._key(identifier)
data = {"status": status}
if metadata:
data.update(metadata)
if use_add:
return cache.add(key, data, timeout or self.queue_status_timeout)
cache.set(key, data, timeout or self.default_timeout)
def get(self, identifier):
return cache.get(self._key(identifier), {})
def get_status(self, identifier):
if not self.enabled:
return
record = cache.get(self._key(identifier), {})
return record.get("status")
def get_ttl(self, identifier):
return cache.ttl(self._key(identifier))
def clear(self, identifier):
if not self.enabled:
return
cache.delete(self._key(identifier))
account_secret_task_status = AccountSecretTaskStatus()

View File

@@ -8,6 +8,7 @@ class ActionChoices(models.TextChoices):
review = 'review', _('Review') review = 'review', _('Review')
warning = 'warning', _('Warn') warning = 'warning', _('Warn')
notice = 'notice', _('Notify') notice = 'notice', _('Notify')
notify_and_warn = 'notify_and_warn', _('Notify and warn') notify_and_warn = 'notify_and_warn', _('Prompt and warn')
face_verify = 'face_verify', _('Face Verify') face_verify = 'face_verify', _('Face verify')
face_online = 'face_online', _('Face Online') face_online = 'face_online', _('Face online')
change_secret = 'change_secret', _('Secret rotation')

View File

@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from common.db.fields import JSONManyToManyField from common.db.fields import JSONManyToManyField
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
from common.utils import contains_ip from common.utils import contains_ip
from common.utils.time_period import contains_time_period from common.utils.timezone import contains_time_period
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from ..const import ActionChoices from ..const import ActionChoices

View File

@@ -34,16 +34,16 @@ class CommandGroup(JMSOrgBaseModel):
@lazyproperty @lazyproperty
def pattern(self): def pattern(self):
content = self.content.replace('\r\n', '\n')
if self.type == 'command': if self.type == 'command':
s = self.construct_command_regex(self.content) s = self.construct_command_regex(content)
else: else:
s = r'{0}'.format(self.content) s = r'{0}'.format(r'{}'.format('|'.join(content.split('\n'))))
return s return s
@classmethod @classmethod
def construct_command_regex(cls, content): def construct_command_regex(cls, content):
regex = [] regex = []
content = content.replace('\r\n', '\n')
for _cmd in content.split('\n'): for _cmd in content.split('\n'):
cmd = re.sub(r'\s+', ' ', _cmd) cmd = re.sub(r'\s+', ' ', _cmd)
cmd = re.escape(cmd) cmd = re.escape(cmd)

View File

@@ -79,6 +79,8 @@ class ActionAclSerializer(serializers.Serializer):
field_action._choices.pop(ActionChoices.face_online, None) field_action._choices.pop(ActionChoices.face_online, None)
for choice in self.Meta.action_choices_exclude: for choice in self.Meta.action_choices_exclude:
field_action._choices.pop(choice, None) field_action._choices.pop(choice, None)
if not settings.XPACK_LICENSE_IS_VALID or not settings.CHANGE_SECRET_AFTER_SESSION_END:
field_action._choices.pop(ActionChoices.change_secret, None)
class BaseACLSerializer(ActionAclSerializer, serializers.Serializer): class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):

View File

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

View File

@@ -14,5 +14,10 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
if i not in ['assets', 'accounts'] if i not in ['assets', 'accounts']
] ]
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [ action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
ActionChoices.review, ActionChoices.accept, ActionChoices.notice ActionChoices.review,
ActionChoices.accept,
ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online,
ActionChoices.change_secret
] ]

View File

@@ -18,7 +18,13 @@ class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
class Meta(BaseUserACLSerializer.Meta): class Meta(BaseUserACLSerializer.Meta):
model = LoginACL model = LoginACL
fields = BaseUserACLSerializer.Meta.fields + ['rules', ] fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
action_choices_exclude = [ActionChoices.face_online, ActionChoices.face_verify] action_choices_exclude = [
ActionChoices.warning,
ActionChoices.notify_and_warn,
ActionChoices.face_online,
ActionChoices.face_verify,
ActionChoices.change_secret
]
def get_rules_serializer(self): def get_rules_serializer(self):
return RuleSerializer() return RuleSerializer()

View File

@@ -1,5 +1,7 @@
# coding: utf-8 # coding: utf-8
# #
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@@ -8,7 +10,7 @@ from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text'] __all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text', 'address_validator']
def ip_group_child_validator(ip_group_child): def ip_group_child_validator(ip_group_child):
@@ -21,6 +23,19 @@ def ip_group_child_validator(ip_group_child):
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
def address_validator(value):
parsed = urlparse(value)
is_basic_url = parsed.scheme in ('http', 'https') and parsed.netloc
is_valid = value == '*' \
or is_ip_address(value) \
or is_ip_network(value) \
or is_ip_segment(value) \
or is_basic_url
if not is_valid:
error = _('address invalid: `{}`').format(value)
raise serializers.ValidationError(error)
ip_group_help_text = _( ip_group_help_text = _(
'With * indicating a match all. ' 'With * indicating a match all. '
'Such as: ' 'Such as: '

View File

@@ -1,10 +1,10 @@
from .asset import * from .asset import *
from .category import * from .category import *
from .domain import *
from .favorite_asset import * from .favorite_asset import *
from .mixin import * from .mixin import *
from .my_asset import *
from .node import * from .node import *
from .platform import * from .platform import *
from .protocol import * from .protocol import *
from .tree import * from .tree import *
from .my_asset import * from .zone import *

View File

@@ -3,6 +3,7 @@ from .cloud import *
from .custom import * from .custom import *
from .database import * from .database import *
from .device import * from .device import *
from .ds import *
from .gpt import * from .gpt import *
from .host import * from .host import *
from .permission import * from .permission import *

View File

@@ -11,6 +11,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK from rest_framework.status import HTTP_200_OK
from accounts.serializers import AccountSerializer
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
from assets import serializers from assets import serializers
from assets.exceptions import NotSupportedTemporarilyError from assets.exceptions import NotSupportedTemporarilyError
@@ -36,12 +37,12 @@ class AssetFilterSet(BaseFilterSet):
platform = drf_filters.CharFilter(method='filter_platform') platform = drf_filters.CharFilter(method='filter_platform')
is_gateway = drf_filters.BooleanFilter(method='filter_is_gateway') is_gateway = drf_filters.BooleanFilter(method='filter_is_gateway')
exclude_platform = drf_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True) exclude_platform = drf_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
domain = drf_filters.CharFilter(method='filter_domain') zone = drf_filters.CharFilter(method='filter_zone')
type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact") type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact") category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
protocols = drf_filters.CharFilter(method='filter_protocols') protocols = drf_filters.CharFilter(method='filter_protocols')
domain_enabled = drf_filters.BooleanFilter( gateway_enabled = drf_filters.BooleanFilter(
field_name="platform__domain_enabled", lookup_expr="exact" field_name="platform__gateway_enabled", lookup_expr="exact"
) )
ping_enabled = drf_filters.BooleanFilter( ping_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__ping_enabled", lookup_expr="exact" field_name="platform__automation__ping_enabled", lookup_expr="exact"
@@ -84,11 +85,11 @@ class AssetFilterSet(BaseFilterSet):
return queryset return queryset
@staticmethod @staticmethod
def filter_domain(queryset, name, value): def filter_zone(queryset, name, value):
if is_uuid(value): if is_uuid(value):
return queryset.filter(domain_id=value) return queryset.filter(zone_id=value)
else: else:
return queryset.filter(domain__name__contains=value) return queryset.filter(zone__name__contains=value)
@staticmethod @staticmethod
def filter_protocols(queryset, name, value): def filter_protocols(queryset, name, value):
@@ -96,10 +97,10 @@ class AssetFilterSet(BaseFilterSet):
return queryset.filter(protocols__name__in=value).distinct() return queryset.filter(protocols__name__in=value).distinct()
class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet): class BaseAssetViewSet(OrgBulkModelViewSet):
"""
API endpoint that allows Asset to be viewed or edited.
""" """
API endpoint that allows Asset to be viewed or edited.
"""
model = Asset model = Asset
filterset_class = AssetFilterSet filterset_class = AssetFilterSet
search_fields = ("name", "address", "comment") search_fields = ("name", "address", "comment")
@@ -109,18 +110,19 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
("platform", serializers.PlatformSerializer), ("platform", serializers.PlatformSerializer),
("suggestion", serializers.MiniAssetSerializer), ("suggestion", serializers.MiniAssetSerializer),
("gateways", serializers.GatewaySerializer), ("gateways", serializers.GatewaySerializer),
("accounts", AccountSerializer),
) )
rbac_perms = ( rbac_perms = (
("match", "assets.match_asset"), ("match", "assets.match_asset"),
("platform", "assets.view_platform"), ("platform", "assets.view_platform"),
("gateways", "assets.view_gateway"), ("gateways", "assets.view_gateway"),
("accounts", "assets.view_account"),
("spec_info", "assets.view_asset"), ("spec_info", "assets.view_asset"),
("gathered_info", "assets.view_asset"), ("gathered_info", "assets.view_asset"),
("sync_platform_protocols", "assets.change_asset"), ("sync_platform_protocols", "assets.change_asset"),
) )
extra_filter_backends = [ extra_filter_backends = [
IpInFilterBackend, IpInFilterBackend, NodeFilterBackend, AttrRulesFilterBackend
NodeFilterBackend, AttrRulesFilterBackend
] ]
def perform_destroy(self, instance): def perform_destroy(self, instance):
@@ -141,6 +143,25 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
return retrieve_cls return retrieve_cls
return cls return cls
def paginate_queryset(self, queryset):
page = super().paginate_queryset(queryset)
if page:
page = Asset.compute_all_accounts_amount(page)
return page
def create(self, request, *args, **kwargs):
if request.path.find('/api/v1/assets/assets/') > -1:
error = _('Cannot create asset directly, you should create a host or other')
return Response({'error': error}, status=400)
if not settings.XPACK_LICENSE_IS_VALID and self.model.objects.order_by().count() >= 5000:
error = _('The number of assets exceeds the limit of 5000')
return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs)
class AssetViewSet(SuggestionMixin, BaseAssetViewSet):
@action(methods=["GET"], detail=True, url_path="platform") @action(methods=["GET"], detail=True, url_path="platform")
def platform(self, *args, **kwargs): def platform(self, *args, **kwargs):
asset = super().get_object() asset = super().get_object()
@@ -150,10 +171,10 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
@action(methods=["GET"], detail=True, url_path="gateways") @action(methods=["GET"], detail=True, url_path="gateways")
def gateways(self, *args, **kwargs): def gateways(self, *args, **kwargs):
asset = self.get_object() asset = self.get_object()
if not asset.domain: if not asset.zone:
gateways = Gateway.objects.none() gateways = Gateway.objects.none()
else: else:
gateways = asset.domain.gateways gateways = asset.zone.gateways
return self.get_paginated_response_from_queryset(gateways) return self.get_paginated_response_from_queryset(gateways)
@action(methods=['post'], detail=False, url_path='sync-platform-protocols') @action(methods=['post'], detail=False, url_path='sync-platform-protocols')
@@ -189,17 +210,6 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
Protocol.objects.bulk_create(objs) Protocol.objects.bulk_create(objs)
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
def create(self, request, *args, **kwargs):
if request.path.find('/api/v1/assets/assets/') > -1:
error = _('Cannot create asset directly, you should create a host or other')
return Response({'error': error}, status=400)
if not settings.XPACK_LICENSE_IS_VALID and self.model.objects.order_by().count() >= 5000:
error = _('The number of assets exceeds the limit of 5000')
return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs)
def filter_bulk_update_data(self): def filter_bulk_update_data(self):
bulk_data = [] bulk_data = []
skip_assets = [] skip_assets = []

View File

@@ -1,12 +1,12 @@
from assets.models import Cloud, Asset from assets.models import Cloud, Asset
from assets.serializers import CloudSerializer from assets.serializers import CloudSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['CloudViewSet'] __all__ = ['CloudViewSet']
class CloudViewSet(AssetViewSet): class CloudViewSet(BaseAssetViewSet):
model = Cloud model = Cloud
perm_model = Asset perm_model = Asset

View File

@@ -1,12 +1,12 @@
from assets.models import Custom, Asset from assets.models import Custom, Asset
from assets.serializers import CustomSerializer from assets.serializers import CustomSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['CustomViewSet'] __all__ = ['CustomViewSet']
class CustomViewSet(AssetViewSet): class CustomViewSet(BaseAssetViewSet):
model = Custom model = Custom
perm_model = Asset perm_model = Asset

View File

@@ -1,12 +1,12 @@
from assets.models import Database, Asset from assets.models import Database, Asset
from assets.serializers import DatabaseSerializer from assets.serializers import DatabaseSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['DatabaseViewSet'] __all__ = ['DatabaseViewSet']
class DatabaseViewSet(AssetViewSet): class DatabaseViewSet(BaseAssetViewSet):
model = Database model = Database
perm_model = Asset perm_model = Asset

View File

@@ -1,11 +1,11 @@
from assets.serializers import DeviceSerializer
from assets.models import Device, Asset from assets.models import Device, Asset
from .asset import AssetViewSet from assets.serializers import DeviceSerializer
from .asset import BaseAssetViewSet
__all__ = ['DeviceViewSet'] __all__ = ['DeviceViewSet']
class DeviceViewSet(AssetViewSet): class DeviceViewSet(BaseAssetViewSet):
model = Device model = Device
perm_model = Asset perm_model = Asset

View File

@@ -0,0 +1,16 @@
from assets.models import DirectoryService, Asset
from assets.serializers import DSSerializer
from .asset import BaseAssetViewSet
__all__ = ['DSViewSet']
class DSViewSet(BaseAssetViewSet):
model = DirectoryService
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = DSSerializer
return serializer_classes

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