Compare commits

...

476 Commits

Author SHA1 Message Date
feng
d1f3aa21be perf: Top session asset user cache 2025-12-09 17:58:58 +08:00
Chenyang Shen
2b1fdb937b Merge pull request #16404 from jumpserver/pr@dev@feat_reset_key_store
feat: reset piico device after open device
2025-12-09 15:16:41 +08:00
Aaron3S
1e754546f1 feat: reset piico device after open device 2025-12-09 14:47:37 +08:00
Bai
2ec71feafc perf: rbac oauth2_provider perms i18n 2025-12-09 10:17:34 +08:00
Bai
02e8905330 perf: redirect/confirm page and i18n 2025-12-08 18:43:04 +08:00
Bai
8d68f5589b perf: redirect/confirm page and i18n 2025-12-08 18:43:04 +08:00
Bai
4df13fc384 perf: redirect/confirm page and i18n 2025-12-08 18:40:12 +08:00
Bai
78c1162028 perf: when DEBUG_DEV=True, allow OAUTH2_PROVIDER redirect_url localhost listen 2025-12-08 16:42:07 +08:00
Bai
14c2512b45 fix: accesskey authentication user is None error 2025-12-08 15:06:47 +08:00
Bai
d6d7072da5 perf: request.GET.copy() to dict(), because copy() returned values is list [] 2025-12-08 12:50:49 +08:00
fit2bot
993bc36c5e perf: handling the next parameter propagation issue in third-party authentication flows (#16395)
* perf: remove call client old- method via ?next=client

* feat: add 2 decorators for login-get and login-callback-get to set next_page and get next_page from session

* perf: code style

* perf: handling the next parameter propagation issue in third-party authentication flows

* perf: request.GET.dict() to copy()

* perf: style import

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-08 12:34:32 +08:00
fit2bot
ecff2ea07e perf: move oauth2_provider api auth_backend to the end, and while accesstoken_backend not user do not raise execption, go on next bakcned auth (#16393)
* perf: move oauth2_provider api auth_backend to the end, and while accesstoken_backend not user do not raise execption, go on next bakcned auth

* perf: re-sorted DEFAULT_AUTHENTICATION_CLASSES

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-08 09:57:17 +08:00
fit2bot
ba70edf221 perf: when oauth2 application delete expired well-known page cache via post_delete signal (#16392)
Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-08 09:54:18 +08:00
Bai
50050dff57 fix: cas only allow exist user login 2025-12-04 18:37:54 +08:00
jiangweidong
944226866c perf: Add a diff field to operate-log export 2025-12-04 18:01:01 +08:00
fit2bot
fe13221d88 fix: Improve server URI validation and connection testing in LDAP module (#16377)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-04 17:59:01 +08:00
fit2bot
ba17863892 perf: Remove unused CAS user exception handling and simplify login view error response (#16380)
* perf: Remove unused CAS user exception handling and simplify login view error response

* perf: position code

---------

Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-04 17:49:58 +08:00
fit2bot
065bfeda52 fix: only exists user login maybe invalid (#16379)
* fix: only exists user login maybe invalid

* fix: only exists user login maybe invalid

* fix: only exists user login maybe invalid

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-04 16:18:47 +08:00
wangruidong
04af26500a fix: Allow login with username or email for existing users 2025-12-04 10:04:32 +08:00
fit2bot
e0388364c3 fix: use third part authentication service rediect to client failed (#16370)
* perf: .well-known cached 1h and support saml2 redirect_to client

* fix: support wecom redirect_to client (reslove wecom waf 501 error)

* fix: support oauth2 auth rediect to client

* fix: safe next url

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-03 19:07:00 +08:00
Bai
3c96480b0c perf: add manage.py command: init_oauth2_provider, resolve init jumpserver client failed issue 2025-12-03 14:37:20 +08:00
Bai
95331a0c4b perf: redirect to client show tips 2025-12-02 18:39:48 +08:00
Bai
b8ecb703cf perf: url revoke_token/ to revoke/ 2025-12-02 18:21:13 +08:00
Bai
1a3f5e3f9a perf: default access token/refresh token expired at 1h/7day 2025-12-02 15:34:55 +08:00
Bai
854396e8d5 perf: access-token api 2025-12-02 15:25:55 +08:00
Bai
ab08603e66 perf: organize oauth2_provider urls, add .well-known API 2025-12-02 14:55:09 +08:00
Bai
427fd3f72c perf: organize oauth2_provider urls, add .well-known API 2025-12-02 14:55:09 +08:00
Bai
0aba9ba120 perf: hide the unused URLs in OAuth2 provider 2025-12-02 14:55:09 +08:00
Bai
045ca8807a feat: modify client redirect url 2025-12-01 19:04:19 +08:00
Bai
19a68d8930 feat: add api access token 2025-12-01 17:55:08 +08:00
Bai
75ed02a2d2 feat: add oauth2 provider accesstokens api 2025-12-01 17:55:08 +08:00
fit2bot
f420dac49c feat: Host cloud sync supports state cloud - i18n (#16304)
Co-authored-by: jiangweidong <1053570670@qq.com>
Co-authored-by: Jiangjie Bai <jiangjie.bai@fit2cloud.com>
2025-12-01 10:56:14 +08:00
Bai
1ee68134f2 fix: rename utils methond 2025-12-01 10:41:14 +08:00
Bai
937265db5d perf: add period task clear oauth2 provider expired tokens 2025-12-01 10:41:14 +08:00
Bai
c611d5e88b perf: add utils delete oauth2 provider application 2025-12-01 10:41:14 +08:00
Bai
883b6b6383 perf: skip_authorization for redirect to jms client 2025-12-01 10:41:14 +08:00
Bai
ac4c72064f perf: register jumpserver client logic 2025-12-01 10:41:14 +08:00
Bai
dbf8360e27 feat: add OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS 2025-12-01 10:41:14 +08:00
github-actions[bot]
150d7a09bc perf: Update Dockerfile with new base image tag 2025-11-28 16:28:23 +08:00
Bai
a7ed20e059 perf: support as oauth2 provider 2025-11-28 16:28:23 +08:00
github-actions[bot]
1b7b8e6f2e perf: Update Dockerfile with new base image tag 2025-11-28 16:28:23 +08:00
Bai
cd22fbce19 perf: support as oauth2 provider 2025-11-28 16:28:23 +08:00
老广
c191d86f43 Refactor GitHub Actions workflow for event handling 2025-11-27 14:27:27 +08:00
wangruidong
7911137ffb fix: Truncate asset URL to 128 characters to prevent exceeding length limit 2025-11-27 14:17:19 +08:00
wangruidong
1053933cae fix: Add migration to refresh PostgreSQL collation version 2025-11-27 14:16:44 +08:00
wangruidong
96fdc025cd fix: Search for risk_level, search result is empty 2025-11-26 18:07:20 +08:00
wangruidong
fde19764e0 fix: Processing redirection url unquote 2025-11-25 14:00:31 +08:00
wangruidong
978fbc70e6 perf: Improve city retrieval fallback to handle missing values 2025-11-25 13:59:48 +08:00
Ewall555
636ffd786d feat: add namespace setting to k8s protocol configuration 2025-11-25 11:08:23 +08:00
feng
3b756aa26f perf: Component i18n lang lower 2025-11-25 10:56:37 +08:00
Bai
817c0099d1 perf: client pkg rename 2025-11-21 18:45:49 +08:00
Bai
a0d7871130 perf: client pkg rename 2025-11-21 18:45:49 +08:00
Bai
c97124c279 perf: client pkg rename 2025-11-20 17:59:22 +08:00
Bai
32a766ed34 perf: client pkg rename 2025-11-20 17:59:22 +08:00
Bai
58fd15d743 perf: client pkg rename 2025-11-20 17:59:22 +08:00
feng
f50250dedb perf: Client version 2025-11-20 16:37:23 +08:00
wangruidong
9e150b7fbe fix: One login lock, resulting in two logs 2025-11-20 15:01:06 +08:00
wangruidong
16c79f59a7 fix: Handle case where all time_periods have empty values as a selection of all 2025-11-20 11:31:09 +08:00
wangruidong
be0f04862a fix: Correctly pass runas value in ACL check for job execution 2025-11-19 19:08:29 +08:00
feng
1a3fb2f0db perf: Account bulk error prompt 2025-11-19 17:42:39 +08:00
Eric
4cd70efe66 perf: fix mp4 type replay 2025-11-19 17:10:26 +08:00
wangruidong
28700c01c8 perf: The login log records the locked login log 2025-11-19 17:08:55 +08:00
wangruidong
4524822245 fix: Solve this version of Mysql doesn't yet support 'LIMIT & IN/ALL/ANY/S0ME subquery' error 2025-11-19 09:52:05 +08:00
Eric
9d04fda018 perf: add match perm to user for suggestions api 2025-11-19 09:48:31 +08:00
老广
01c277cd1e Add Client to JumpServer components list 2025-11-19 09:19:52 +08:00
wangruidong
c4b3531d72 fix: correct handling of changed field values in operate log 2025-11-18 10:32:49 +08:00
feng
8870d1ef9e perf: Translate 2025-11-17 18:25:40 +08:00
wangruidong
6c5086a083 perf: implement login asset ACL checks in Job and JobExecution viewsets 2025-11-17 10:53:22 +08:00
wrd
e9f762a982 Revert "perf: Reduce the number of pub sub processing threads (#16072)"
This reverts commit 70068c9253.
2025-11-17 10:52:16 +08:00
wangruidong
d4d4cadbcd fix: OAuth2 Only allow existing users to log in operate log error 2025-11-13 18:42:28 +08:00
fit2bot
5e56590405 perf: change base img (#16279)
* perf: change base img

* perf: add gcc

* perf: change base image

* 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-11-13 17:32:51 +08:00
wangruidong
ad8c0f6664 fix: SAML2 Only allow existing users to log in operate log error 2025-11-13 16:36:58 +08:00
wangruidong
47dd6babfc perf: add id verbose_name 2025-11-13 15:17:14 +08:00
ibuler
691d1c4dba perf: remove client key 2025-11-13 14:36:40 +08:00
ibuler
ac485804d5 perf: postgresql support ssl 2025-11-13 14:36:40 +08:00
ibuler
51e5fdb301 perf: change i18n 2025-11-13 10:05:37 +08:00
feng
69c4d613f7 perf: Add client support version 2025-11-11 16:37:12 +08:00
github-actions[bot]
1ad825bf0d perf: Update Dockerfile with new base image tag 2025-11-11 15:11:51 +08:00
ibuler
a286cb9343 deps: upgrade playwright 2025-11-11 15:11:51 +08:00
ibuler
1eb489bb2d perf: upgrade pg client 2025-11-11 14:24:53 +08:00
fit2bot
4334ae9e5e perf: update apt source config (#16265)
* perf: upgrade os to trixie

* perf: update apt source config

* 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-11-11 14:17:35 +08:00
fit2bot
f2e346a0c3 perf: upgrade os to trixie (#16263)
* perf: upgrade os to trixie

* 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-11-11 11:52:17 +08:00
wangruidong
dc20b06431 fix: i18n error 2025-11-10 18:14:18 +08:00
fit2bot
387a9248fc perf: Add a key to cover all protocols and ports (#16227)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-10 18:04:00 +08:00
wangruidong
705fd6385f fix: i18n error 2025-11-10 18:03:51 +08:00
fit2bot
0ccf36621f perf: Translate select files (#16212)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-06 18:26:54 +08:00
fit2bot
a9ae12fc2c perf: Implement data masking rules ACL check before job execution (#16216)
* perf: Implement data masking rules ACL check before job execution

* perf: Add login asset ACL check during job creation

* perf: Remove unused code.

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-11-06 18:25:34 +08:00
老广
7b1a25adde Add issue spam configuration file 2025-11-06 18:13:42 +08:00
feng
a1b5eb1cd8 perf: Translate 2025-11-06 15:50:15 +08:00
wangruidong
24ac642c5e fix: Escape percentage signs in gateway password for sshpass command 2025-11-06 14:10:24 +08:00
wangruidong
e4f5e21219 perf: Support batch import of leak passwords 2025-11-06 14:09:09 +08:00
feng
a2aae9db47 perf: Translate 2025-11-05 19:07:48 +08:00
feng
206c43cf75 fix: Fixed the issue of inaccurate calculation of the number of dashboard commands. 2025-11-04 18:14:02 +08:00
feng
019a657ec3 perf: Ssotoken login create operator choose org_id 2025-11-03 17:36:04 +08:00
feng
fad60ee40f perf: Translate 2025-11-03 10:51:22 +08:00
feng
1728412793 perf: Bulk account support node 2025-10-31 17:19:48 +08:00
feng
3e93034fbc perf: Update remote_client 2025-10-30 10:12:40 +08:00
feng
f4b3a7d73a perf: Sync feishu info 2025-10-29 14:53:45 +08:00
wrd
3781c40179 Revert "perf: update fields serialization and bump django and djangorestframe…"
This reverts commit dd0cacb4bc.
2025-10-29 11:19:50 +08:00
ibuler
fab6219cea perf: branches auto cleanup 2025-10-29 10:10:21 +08:00
fit2bot
dd0cacb4bc perf: update fields serialization and bump django and djangorestframework versions (#16209)
Co-authored-by: wangruidong <940853815@qq.com>
2025-10-28 16:42:06 +08:00
ibuler
b8639601a1 perf: branches auto cleanup 2025-10-27 15:33:06 +08:00
老广
ab9882c9c1 perf: check api summary 2025-10-27 15:28:21 +08:00
ibuler
77a7b74b15 perf: print summary in the end 2025-10-27 15:26:04 +08:00
dependabot[bot]
4bc05865f1 chore(deps): bump python-ldap from 3.4.3 to 3.4.5
Bumps [python-ldap](https://github.com/python-ldap/python-ldap) from 3.4.3 to 3.4.5.
- [Release notes](https://github.com/python-ldap/python-ldap/releases)
- [Changelog](https://github.com/python-ldap/python-ldap/blob/python-ldap-3.4.5/CHANGES)
- [Commits](https://github.com/python-ldap/python-ldap/compare/python-ldap-3.4.3...python-ldap-3.4.5)

---
updated-dependencies:
- dependency-name: python-ldap
  dependency-version: 3.4.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 15:24:06 +08:00
fit2bot
bec9e4f3a7 perf: update deps kombu (#16133)
* perf: update deps kombu

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: Ewall555 <a03216@foxmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: wrd <940853815@qq.com>
2025-10-27 15:18:16 +08:00
fit2bot
359adf3dbb perf: add check api for common user 2025-10-27 14:54:02 +08:00
feng
ac54bb672c fix: Bulk account invalid secret_reset 2025-10-24 18:18:16 +08:00
ibuler
9e3ba00bc4 perf: search support keyword q=str to search 2025-10-24 10:22:49 +08:00
wangruidong
2ec9a43317 fix: Any change to the LDAP server URI should require re-authentication and explicit re-entry of
the bind password, not reuse stored credentials
2025-10-23 15:29:47 +08:00
wangruidong
06be56ef06 fix: Enhance state check to include query parameter for session validation 2025-10-23 14:41:50 +08:00
ibuler
b2a618b206 perf: user sugguestion limit and serializer 2025-10-23 14:40:37 +08:00
wangruidong
1039c2e320 perf: ws/ldap perms check 2025-10-23 14:26:24 +08:00
fit2bot
8d7267400d fix: OpenID Only allow existing users to log in operate log error (#16013)
Co-authored-by: wangruidong <940853815@qq.com>
2025-10-22 14:53:12 +08:00
ibuler
d67e473884 perf: add auto cleanup branches 2025-10-22 11:46:09 +08:00
fit2bot
70068c9253 perf: Reduce the number of pub sub processing threads (#16072)
* perf: Reduce the number of pub sub processing threads

* perf: Using thread pool to process messages

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-10-21 17:41:14 +08:00
wangruidong
d68babb2e1 fix: Using winrm protocol to transfer files did not create a directory problem 2025-10-21 17:31:41 +08:00
wangruidong
afb6f466d5 perf: AppletHost translate 2025-10-21 17:31:03 +08:00
ibuler
453ad331ee perf: token retrieve 2025-10-21 10:48:08 +08:00
feng
d309d11a8f perf: Command count 2025-10-16 17:11:42 +08:00
feng
4771693a56 fix: dashboard command count 2025-10-16 16:25:01 +08:00
Chenyang Shen
cefc820ac1 Merge pull request #16163 from jumpserver/pr@dev@asset_acl_filter
perf: Asset acl filter action
2025-10-16 15:25:38 +08:00
feng
d007afdb43 perf: Asset acl filter action 2025-10-16 15:21:32 +08:00
feng
e8921a43be perf: Translate 2025-10-16 14:32:59 +08:00
wangruidong
a9b44103d4 fix: Handle email sending failure with appropriate error response 2025-10-16 11:28:41 +08:00
jiangweidong
4abf2bded6 perf: oracle cdb mode, common users need to start username with C## 2025-10-16 09:57:54 +08:00
feng
54693089a0 perf: replace command objects 2025-10-15 19:32:14 +08:00
Aaron3S
0b859dd502 feat: update i18n 2025-10-15 19:17:44 +08:00
feng
3fb27f969a perf: datamaskingrule perm 2025-10-15 17:33:27 +08:00
Aaron3S
45627a1d92 feat: update data masking rule filter 2025-10-15 16:51:58 +08:00
feng
245e2dab66 perf: Filter effective 2025-10-15 16:51:32 +08:00
Aaron3S
8f0a41b1a8 fix: fix data masking org problem 2025-10-15 15:51:14 +08:00
feng
1a9e56c520 perf: Translate 2025-10-15 15:24:19 +08:00
feng
67c2f471b4 perf: oracle sqlserver db2 dameng clickhouse redis db_name allow_blank 2025-10-15 11:30:00 +08:00
github-actions[bot]
b04f96f5f2 perf: Update Dockerfile with new base image tag 2025-10-14 18:09:25 +08:00
Eric
30f03b7d89 perf: change python base
perf: update deps
2025-10-14 18:09:25 +08:00
wangruidong
28a97d0b5a fix: Incorrect language display in some email content 2025-10-14 18:08:21 +08:00
Eric
3410686690 perf: fix python base ci 2025-10-14 17:47:31 +08:00
Eric
6860e2327f perf: add python base ci build 2025-10-14 17:41:05 +08:00
feng
20253e760c perf: translate 2025-10-14 17:13:42 +08:00
Aaron3S
a63cfde8d2 feat: add translate 2025-10-14 16:03:38 +08:00
feng
92e250e03b perf: user_can_authenticate add logger 2025-10-14 15:48:47 +08:00
wangruidong
098f0950cb fix: Incorrect language display in email content 2025-10-14 15:33:04 +08:00
feng
39b0830a6b perf: web script default [] 2025-10-14 13:59:11 +08:00
wangruidong
2e847bc2bc fix: Error in updating message subscription 500 2025-10-14 10:14:50 +08:00
wangruidong
f82f31876a fix: Mysql has set a gateway, and the command execution failed. 2025-10-14 10:14:23 +08:00
github-actions[bot]
cde182c015 perf: Update Dockerfile with new base image tag 2025-10-10 17:06:14 +08:00
Eric
b990cdf561 perf: update deps 2025-10-10 17:06:14 +08:00
feng
c9a062823d perf: Translate 2025-10-10 17:02:30 +08:00
feng
643ba4fc15 fix: Asset web script dont create 2025-10-10 11:43:11 +08:00
feng
d16a55bbe2 perf: Ticket details cannot view assets from other organizations. 2025-10-09 18:41:25 +08:00
fit2bot
ae31554729 perf: AppletHostOnly label match (#16109)
Co-authored-by: wangruidong <940853815@qq.com>
2025-10-09 18:13:37 +08:00
github-actions[bot]
53b47980a2 perf: Update Dockerfile with new base image tag 2025-10-09 16:55:50 +08:00
Eric
d31b5ee570 perf: update Dockerfile-base 2025-10-09 16:55:50 +08:00
feng
65aea1ea36 perf: Push account and change secret support gid 2025-10-09 16:39:32 +08:00
feng
5abb5c5d5a perf: Themes deep blue 2025-10-09 15:36:14 +08:00
feng
93e41a5004 perf: Luna themes default 2025-10-09 15:02:37 +08:00
feng
95f51bbe48 perf: Perference add themes 2025-10-09 14:47:11 +08:00
feng
0184d292ec perf: MFA code 2025-10-09 14:29:08 +08:00
fit2bot
23a6d320c7 feat: update i18n (#16101)
* feat: data masking

* feat: update i18n

---------

Co-authored-by: Aaron3S <chenyang@fit2cloud.com>
Co-authored-by: 老广 <ibuler@qq.com>
2025-10-09 10:03:11 +08:00
Aaron3S
b16304c48a feat: data masking 2025-10-09 09:59:23 +08:00
Gerry.tan
7cd1e4d3a0 perf: Dynamically configure the validity period of the email verification code 2025-09-28 11:26:32 +08:00
Eric
64a9987c3f perf: update rdp params 2025-09-28 11:20:52 +08:00
feng
18bfe312fa perf: open web ui 2025-09-25 15:49:10 +08:00
wangruidong
c593f91d77 fix: Account backup: when sending to the mailbox fails, the task status also shows the success problem. 2025-09-18 15:44:35 +08:00
feng
46da05652a fix: Fixed the issue where the final connection verification failed when the domain name contains . 2025-09-18 14:08:00 +08:00
feng
9249aba1a9 perf: Video player version 2025-09-18 11:03:58 +08:00
fit2bot
eca637c120 perf: Translate msg template (#16050)
* fix: Correct translation for device and user limits in django.po

* perf: Translate msg template

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-09-17 19:04:06 +08:00
feng
ddacd5fce1 fix: Ticket direct approval 2025-09-17 18:58:16 +08:00
wangruidong
3ca5c04099 fix: Add ignore_https_errors option to browser context 2025-09-17 16:30:54 +08:00
wangruidong
6603a073ec fix: Case 2025-09-17 15:32:23 +08:00
wangruidong
d745f7495a fix: Conflict 2025-09-17 15:32:23 +08:00
wangruidong
76f1667c89 perf: Restore msg template default value config 2025-09-17 15:32:23 +08:00
wangruidong
1ab1954299 fix: reset password msg error 2025-09-17 15:32:23 +08:00
wangruidong
c8335999a4 perf: Translate msg template 2025-09-17 15:32:23 +08:00
feng
5b4a67362d perf: Translate 2025-09-17 15:10:54 +08:00
fit2bot
e025073da2 fix: The number of exported data is incorrect (#16043)
Co-authored-by: wangruidong <940853815@qq.com>
2025-09-16 18:52:24 +08:00
feng
2155bc6862 perf: Migrate 2025-09-16 16:46:30 +08:00
wangruidong
953b515817 perf: Add is_alive filter to TerminalFilterSet 2025-09-16 16:30:57 +08:00
ibuler
7f7a354b2d fix: get obj error on queryset limit 2025-09-16 16:28:54 +08:00
Eric
2b2f7ea3f0 perf: add rdp true color 24 bit 2025-09-16 16:28:14 +08:00
feng
529123e1b5 perf: Translate 2025-09-16 16:15:09 +08:00
ibuler
e156ab6ad8 fix: force page limit 2025-09-16 13:48:06 +08:00
wangruidong
3c1fd134ae fix: There is something wrong with the format of the site message 2025-09-16 13:33:43 +08:00
Bai
b15f663c87 fix: AK/SK remained valid after the user expired. 2025-09-16 13:32:25 +08:00
wangruidong
93906dff0a fix: Export report pdf failed 2025-09-16 11:36:42 +08:00
Bai
307befdacd fix: login acl action reject > reviewers 500 2025-09-16 11:17:42 +08:00
feng626
dbfc4d3981 Revert "perf: User acl 500"
This reverts commit 849edd33c1.
2025-09-16 11:15:51 +08:00
feng
849edd33c1 perf: User acl 500 2025-09-16 10:50:41 +08:00
feng
37cceec8fe perf: get protocols error 500 2025-09-16 10:40:42 +08:00
feng
d2494c25cc perf: Translate 2025-09-15 19:19:01 +08:00
feng
023952582e fix: Push account failed 2025-09-15 15:32:27 +08:00
halo
863fe95100 perf: client version 2025-09-12 18:53:16 +08:00
wangruidong
4b0bdb18c9 perf: Template msg example error 2025-09-12 18:47:47 +08:00
Eric
10da053a95 perf: change applet-hosts view default limit 2025-09-12 18:43:38 +08:00
mikebofs
c40bc46520 fix: asset permission exclude accounts with -action 2025-09-12 11:16:27 +08:00
feng
a732cc614e perf: Asset user login notify 2025-09-11 14:16:00 +08:00
ibuler
bb29d519c6 perf: exclude accounts date expired 2025-09-11 11:42:44 +08:00
ibuler
b56c3a76a7 fix: user option error 2025-09-11 11:21:59 +08:00
fit2bot
ab908d24a7 perf: add i18n (#16001)
* perf: change some api view default limit

* perf: add i18n

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
2025-09-10 18:18:18 +08:00
fit2bot
79cabe1b3c feat: setting email template content (#15974)
* feat: setting email template content

* perf: tempale list

* perf: custom template render to string

* perf: content serialize valid

* perf: Custom msg template base class

* perf: Template content reset

* perf: Update templates config

* perf: Remove useless code

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-09-10 16:49:52 +08:00
feng
231b7287c1 perf: Notify info css optimization 2025-09-10 14:04:19 +08:00
feng
be7a4c0d6e perf: Create account unique message 2025-09-09 17:39:18 +08:00
feng
009da19050 perf: Change secret windows password cannot contain > ^ 2025-09-09 16:41:45 +08:00
feng
dfda6b1e08 perf: Change secret del over report 2025-09-09 15:48:03 +08:00
fit2bot
59b40578d8 fix: adhoc SQL Server 2008 (#15984)
* fix: Resolve the issue of errors occurring during automated execution with SQL Server 2008

* fix: adhoc SQL Server 2008

* perf: add todo information

---------

Co-authored-by: halo <wuyihuangw@gmail.com>
2025-09-09 14:26:42 +08:00
Eric
e5db28c014 perf: user add has_public_keys 2025-09-09 14:23:39 +08:00
Eric
6d1f26b0f8 perf: add redis cluster mode setting 2025-09-09 13:51:53 +08:00
Ewall555
2333dbbe33 fix: avoid AttributeError when default_limit is missing 2025-09-09 13:32:52 +08:00
fit2bot
16461b0fa9 perf: support global search (#15961)
* perf: support global search

* perf: change serach

* perf: search model add asset permission

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
Co-authored-by: ibuler <ibuler@qq.com>
2025-09-05 16:40:18 +08:00
mikebofs
528b0ea1ba perf: change some api view default limit 2025-09-05 16:20:26 +08:00
ibuler
60f06adaa9 fix: wechat or phone decrypt err 2025-09-04 11:59:04 +08:00
Bai
7a6187b95f fix: temp token backend 2025-09-03 18:10:10 +08:00
Bai
aacaf3a174 perf: aks encrypt 2025-09-03 11:16:04 +08:00
Bai
3c9d2534fa perf: aks encrypt 2025-09-03 11:16:04 +08:00
wangruidong
4f79abe678 perf: Connect methods acl allow accept action 2025-09-03 11:00:56 +08:00
fit2bot
ae9956ff91 chore: change readme 2025-09-02 15:22:44 +08:00
Bai
429677e0ce perf: readme 2025-09-02 14:54:28 +08:00
ibuler
034ee65157 perf: decrypt secret logic 2025-09-02 10:38:10 +08:00
Eric
fdd7d9b6b1 perf: add vnc client method 2025-09-02 10:34:39 +08:00
wangruidong
db0e21f5d9 fix: Lazy import Azure and Google Cloud dependencies 2025-08-29 11:10:43 +08:00
wangruidong
468b84eb3d perf: Validate connection token id 2025-08-29 11:09:40 +08:00
ibuler
28d5475d0f perf: try to decrypt then origin value 2025-08-29 11:00:02 +08:00
ibuler
b9c60d856f perf: allow some api page no limits 2025-08-28 17:05:11 +08:00
feng
bd1d73c6dd perf: Report localtime 2025-08-28 15:39:54 +08:00
wangruidong
bf92c756d4 fix: Ensure command arguments are safely quoted in safe_run_cmd 2025-08-28 14:14:55 +08:00
feng
62ebe0d636 perf: Third login redirect url query string 2025-08-27 14:45:56 +08:00
github-actions[bot]
0b1fea8492 perf: Update Dockerfile with new base image tag 2025-08-27 11:05:19 +08:00
mikebofs
65b5f573f8 perf: change requirements 2025-08-27 11:05:19 +08:00
mikebofs
bb639e1fe7 perf: revert django-simple-history version 2025-08-27 10:43:21 +08:00
fit2bot
395b868dcf perf: swagger done (#15865)
* perf: swagger upgrade

* perf: upgrade to drf-spectacular

* perf: 添加部分注解

* perf: swagger done

---------

Co-authored-by: ibuler <ibuler@qq.com>
2025-08-27 10:27:01 +08:00
wangruidong
1350b774b3 perf: Improve chart rendering wait logic in export process 2025-08-26 16:20:22 +08:00
wrd
af7a00c1b1 fix: typo 2025-08-26 15:31:13 +08:00
wangruidong
965ec7007c perf: Enhance eager loading by including labels in queryset 2025-08-26 15:31:13 +08:00
fit2bot
1372fd7535 feat: asset permission support exclude some account
* perf: add perm exclude

* perf: exclude node action account

* perf: add i18n

* perf: pop exclude account

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
2025-08-26 14:57:57 +08:00
wangruidong
3b0ef4cca7 fix: Add nmap to Dockerfile dependencies 2025-08-25 16:29:10 +08:00
Aaron3S
6832abdaad feat: change some translate 2025-08-25 11:05:49 +08:00
feng
c6bf290dbb perf: Report translate 2025-08-22 18:57:14 +08:00
feng
23ab66c11a perf: Translate 2025-08-22 18:05:30 +08:00
feng
1debaa5547 perf: report perm 2025-08-22 17:53:52 +08:00
Bai
47413966c9 perf: captcha > CAPTCHA 2025-08-22 16:25:45 +08:00
Eric
703f39607c perf: default allow hosts 2025-08-22 14:12:45 +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
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
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
504 changed files with 50679 additions and 15570 deletions

26
.github/.github/issue-spam-config.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"dry_run": false,
"min_account_age_days": 3,
"max_urls_for_spam": 1,
"min_body_len_for_links": 40,
"spam_words": [
"call now",
"zadzwoń",
"zadzwoń teraz",
"kontakt",
"telefon",
"telefone",
"contato",
"suporte",
"infolinii",
"click here",
"buy now",
"subscribe",
"visit"
],
"bracket_max": 6,
"special_char_density_threshold": 0.12,
"phone_regex": "\\+?\\d[\\d\\-\\s\\(\\)\\.]{6,}\\d",
"labels_for_spam": ["spam"],
"labels_for_review": ["needs-triage"]
}

View File

@@ -1,74 +1,72 @@
name: Build and Push Base Image name: Build and Push Base Image
on: on:
pull_request: pull_request:
branches: branches:
- 'dev' - 'dev'
- 'v*' - 'v*'
paths: paths:
- poetry.lock - poetry.lock
- pyproject.toml - pyproject.toml
- Dockerfile-base - Dockerfile-base
- package.json - package.json
- go.mod - go.mod
- yarn.lock - yarn.lock
- pom.xml - pom.xml
- install_deps.sh - install_deps.sh
- utils/clean_site_packages.sh - utils/clean_site_packages.sh
types: types:
- opened - opened
- synchronize - synchronize
- reopened - reopened
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract date - name: Extract date
id: vars id: vars
run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV
- name: Extract repository name - name: Extract repository name
id: repo id: repo
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
- name: Build and push multi-arch image - name: Build and push multi-arch image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
file: Dockerfile-base file: Dockerfile-base
tags: jumpserver/core-base:${{ env.IMAGE_TAG }} tags: jumpserver/core-base:${{ env.IMAGE_TAG }}
- name: Update Dockerfile - name: Update Dockerfile
run: | run: |
sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile
- name: Commit changes - name: Commit changes
run: | run: |
git config --global user.name 'github-actions[bot]' git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com' git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add Dockerfile git add Dockerfile
git commit -m "perf: Update Dockerfile with new base image tag" git commit -m "perf: Update Dockerfile with new base image tag"
git push origin ${{ github.event.pull_request.head.ref }} git push origin ${{ github.event.pull_request.head.ref }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,46 @@
name: Build and Push Python Base Image
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build'
required: true
default: '3.11-slim-bullseye-v1'
type: string
jobs:
build-and-push:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract repository name
id: repo
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile-python
tags: jumpserver/core-base:python-${{ inputs.tag }}

123
.github/workflows/cleanup-branches.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Cleanup PR Branches
on:
schedule:
# 每天凌晨2点运行
- cron: '0 2 * * *'
workflow_dispatch:
# 允许手动触发
inputs:
dry_run:
description: 'Dry run mode (default: true)'
required: false
default: 'true'
type: boolean
jobs:
cleanup-branches:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取所有分支和提交历史
- name: Setup Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Get dry run setting
id: dry-run
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "dry_run=${{ github.event.inputs.dry_run }}" >> $GITHUB_OUTPUT
else
echo "dry_run=false" >> $GITHUB_OUTPUT
fi
- name: Cleanup branches
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ steps.dry-run.outputs.dry_run }}
run: |
echo "Starting branch cleanup..."
echo "Dry run mode: $DRY_RUN"
# 获取所有本地分支
git fetch --all --prune
# 获取以 pr 或 repr 开头的分支
branches=$(git branch -r | grep -E 'origin/(pr|repr)' | sed 's/origin\///' | grep -v 'HEAD')
echo "Found branches matching pattern:"
echo "$branches"
deleted_count=0
skipped_count=0
for branch in $branches; do
echo ""
echo "Processing branch: $branch"
# 检查分支是否有未合并的PR
pr_info=$(gh pr list --head "$branch" --state open --json number,title,state 2>/dev/null)
if [ $? -eq 0 ] && [ "$pr_info" != "[]" ]; then
echo " ⚠️ Branch has open PR(s), skipping deletion"
echo " PR info: $pr_info"
skipped_count=$((skipped_count + 1))
continue
fi
# 检查分支是否有已合并的PR可选如果PR已合并也可以删除
merged_pr_info=$(gh pr list --head "$branch" --state merged --json number,title,state 2>/dev/null)
if [ $? -eq 0 ] && [ "$merged_pr_info" != "[]" ]; then
echo " ✅ Branch has merged PR(s), safe to delete"
echo " Merged PR info: $merged_pr_info"
else
echo " No PRs found for this branch"
fi
# 执行删除操作
if [ "$DRY_RUN" = "true" ]; then
echo " 🔍 [DRY RUN] Would delete branch: $branch"
deleted_count=$((deleted_count + 1))
else
echo " 🗑️ Deleting branch: $branch"
# 删除远程分支
if git push origin --delete "$branch" 2>/dev/null; then
echo " ✅ Successfully deleted remote branch: $branch"
deleted_count=$((deleted_count + 1))
else
echo " ❌ Failed to delete remote branch: $branch"
fi
fi
done
echo ""
echo "=== Cleanup Summary ==="
echo "Branches processed: $(echo "$branches" | wc -l)"
echo "Branches deleted: $deleted_count"
echo "Branches skipped: $skipped_count"
if [ "$DRY_RUN" = "true" ]; then
echo ""
echo "🔍 This was a DRY RUN - no branches were actually deleted"
echo "To perform actual deletion, run this workflow manually with dry_run=false"
fi
- name: Create summary
if: always()
run: |
echo "## Branch Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Workflow:** ${{ github.workflow }}" >> $GITHUB_STEP_SUMMARY
echo "**Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
echo "**Dry Run:** ${{ steps.dry-run.outputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
echo "**Triggered by:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check the logs above for detailed information about processed branches." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,10 +1,33 @@
on: [push, pull_request, release] on:
push:
pull_request:
types: [opened, synchronize, closed]
release:
types: [created]
name: JumpServer repos generic handler name: JumpServer repos generic handler
jobs: jobs:
generic_handler: handle_pull_request:
name: Run generic handler if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
handle_push:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
handle_release:
if: github.event_name == 'release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: jumpserver/action-generic-handler@master - uses: jumpserver/action-generic-handler@master

View File

@@ -1,11 +1,9 @@
name: 🔀 Sync mirror to Gitee name: 🔀 Sync mirror to Gitee
on: on:
push: schedule:
branches: # 每天凌晨3点运行
- master - cron: '0 3 * * *'
- dev
create:
jobs: jobs:
mirror: mirror:
@@ -14,7 +12,6 @@ jobs:
steps: steps:
- name: mirror - name: mirror
continue-on-error: true continue-on-error: true
if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag')
uses: wearerequired/git-mirror-action@v1 uses: wearerequired/git-mirror-action@v1
env: env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}

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:20250509_094529 AS stage-build FROM jumpserver/core-base:20251128_025056 AS stage-build
ARG VERSION ARG VERSION
@@ -19,7 +19,7 @@ RUN set -ex \
&& python manage.py compilemessages && python manage.py compilemessages
FROM python:3.11-slim-bullseye FROM python:3.11-slim-trixie
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH PATH=/opt/py3/bin:$PATH
@@ -33,12 +33,13 @@ ARG TOOLS=" \
default-libmysqlclient-dev \ default-libmysqlclient-dev \
openssh-client \ openssh-client \
sshpass \ sshpass \
nmap \
bubblewrap" bubblewrap"
ARG APT_MIRROR=http://deb.debian.org ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \ RUN set -ex \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update > /dev/null \ && apt-get update > /dev/null \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \

View File

@@ -1,6 +1,5 @@
FROM python:3.11-slim-bullseye FROM python:3.11.14-slim-trixie
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 \
@@ -22,13 +21,13 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
set -ex \ set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \ && rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources \
&& apt-get update > /dev/null \ && apt-get update > /dev/null \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash && echo "no" | dpkg-reconfigure dash
# Install bin tools # Install bin tools
ARG CHECK_VERSION=v1.0.4 ARG CHECK_VERSION=v1.0.5
RUN set -ex \ RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \ && wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \ && tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
@@ -41,12 +40,10 @@ 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}
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 \ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH PATH=/opt/py3/bin:$PATH
ENV SETUPTOOLS_SCM_PRETEND_VERSION=3.4.5
ENV UV_LINK_MODE=copy
RUN --mount=type=cache,target=/root/.cache \ RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
@@ -54,6 +51,7 @@ RUN --mount=type=cache,target=/root/.cache \
--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 \ --mount=type=bind,source=requirements/static_files.sh,target=utils/static_files.sh \
set -ex \ set -ex \
&& pip install uv -i${PIP_MIRROR} \
&& uv venv \ && uv venv \
&& uv pip install -i${PIP_MIRROR} -r pyproject.toml \ && uv pip install -i${PIP_MIRROR} -r pyproject.toml \
&& ln -sf $(pwd)/.venv /opt/py3 \ && ln -sf $(pwd)/.venv /opt/py3 \

View File

@@ -13,7 +13,9 @@ ARG TOOLS=" \
nmap \ nmap \
telnet \ telnet \
vim \ vim \
wget" postgresql-client \
wget \
poppler-utils"
RUN set -ex \ RUN set -ex \
&& apt-get update \ && apt-get update \
@@ -26,5 +28,5 @@ WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple ARG PIP_MIRROR=https://pypi.org/simple
RUN set -ex \ RUN set -ex \
&& uv pip install -i${PIP_MIRROR} --group xpack && uv pip install -i${PIP_MIRROR} --group xpack \
&& playwright install chromium --with-deps --only-shell

View File

@@ -2,7 +2,7 @@
<a name="readme-top"></a> <a name="readme-top"></a>
<a href="https://jumpserver.com" target="_blank"><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 platform (Bastion Host)
[![][license-shield]][license-link] [![][license-shield]][license-link]
[![][docs-shield]][docs-link] [![][docs-shield]][docs-link]
@@ -12,19 +12,19 @@
[![][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) · [Español](/readmes/README.es.md) · [Русский](/readmes/README.ru.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/>
## What is JumpServer? ## What is JumpServer?
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) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
<picture> <picture>
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f"> <source media="(prefers-color-scheme: light)" srcset="https://www.jumpserver.com/images/jumpserver-arch-light.png">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/28676212-2bc4-4a9f-ae10-3be9320647e3"> <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"> <img src="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f" alt="Theme-based Image">
</picture> </picture>
@@ -77,7 +77,8 @@ JumpServer consists of multiple key components, which collectively form the func
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal | | [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal |
| [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
| [Client](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer Client |
| [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 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 | | [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
@@ -85,6 +86,8 @@ JumpServer consists of multiple key components, which collectively form the func
| [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

View File

@@ -1,16 +1,18 @@
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers as drf_serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.generics import ListAPIView, CreateAPIView
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, HTTP_400_BAD_REQUEST
from accounts import serializers from accounts import serializers
from accounts.const import ChangeSecretRecordStatusChoice from accounts.const import ChangeSecretRecordStatusChoice
from accounts.filters import AccountFilterSet, NodeFilterBackend from accounts.filters import AccountFilterSet, NodeFilterBackend
from accounts.mixins import AccountRecordViewLogMixin from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account, ChangeSecretRecord from accounts.models import Account, ChangeSecretRecord
from assets.const.gpt import create_or_update_chatx_resources
from assets.models import Asset, Node from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin from common.api.mixin import ExtraFilterFieldsMixin
@@ -18,6 +20,7 @@ from common.drf.filters import AttrRulesFilterBackend
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.utils import lazyproperty, get_logger from common.utils import lazyproperty, get_logger
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -41,8 +44,9 @@ 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',
'chat': 'accounts.view_account',
} }
export_as_zip = True export_as_zip = True
@@ -78,18 +82,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)
@@ -145,6 +156,13 @@ class AccountViewSet(OrgBulkModelViewSet):
def copy_to_assets(self, request, *args, **kwargs): def copy_to_assets(self, request, *args, **kwargs):
return self._copy_or_move_to_assets(request, move=False) return self._copy_or_move_to_assets(request, move=False)
@action(methods=['get'], detail=False, url_path='chat')
def chat(self, request, *args, **kwargs):
with tmp_to_root_org():
__, account = create_or_update_chatx_resources()
serializer = self.get_serializer(account)
return Response(serializer.data)
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet): class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
""" """
@@ -167,12 +185,66 @@ class AssetAccountBulkCreateApi(CreateAPIView):
'POST': 'accounts.add_account', 'POST': 'accounts.add_account',
} }
@staticmethod
def get_all_assets(base_payload: dict):
nodes = base_payload.pop('nodes', [])
asset_ids = base_payload.pop('assets', [])
nodes = Node.objects.filter(id__in=nodes).only('id', 'key')
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
asset_ids = set(asset_ids + list(node_asset_ids))
return Asset.objects.filter(id__in=asset_ids)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) if hasattr(request.data, "copy"):
serializer.is_valid(raise_exception=True) base_payload = request.data.copy()
data = serializer.create(serializer.validated_data) else:
serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True) base_payload = dict(request.data)
return Response(data=serializer.data, status=HTTP_200_OK)
templates = base_payload.pop("template", None)
assets = self.get_all_assets(base_payload)
if not assets.exists():
error = _("No valid assets found for account creation.")
return Response(
data={
"detail": error,
"code": "no_valid_assets"
},
status=HTTP_400_BAD_REQUEST
)
result = []
errors = []
def handle_one(_payload):
try:
ser = self.get_serializer(data=_payload)
ser.is_valid(raise_exception=True)
data = ser.bulk_create(ser.validated_data, assets)
if isinstance(data, (list, tuple)):
result.extend(data)
else:
result.append(data)
except drf_serializers.ValidationError as e:
errors.extend(list(e.detail))
except Exception as e:
errors.extend([str(e)])
if not templates:
handle_one(base_payload)
else:
if not isinstance(templates, (list, tuple)):
templates = [templates]
for tpl in templates:
payload = dict(base_payload)
payload["template"] = tpl
handle_one(payload)
if errors:
raise drf_serializers.ValidationError(errors)
out_ser = serializers.AssetAccountBulkSerializerResultSerializer(result, many=True)
return Response(data=out_ser.data, status=HTTP_200_OK)
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView): class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView):
@@ -183,6 +255,7 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
rbac_perms = { rbac_perms = {
'GET': 'accounts.view_accountsecret', 'GET': 'accounts.view_accountsecret',
} }
queryset = Account.history.model.objects.none()
@lazyproperty @lazyproperty
def account(self) -> Account: def account(self) -> Account:

View File

@@ -25,7 +25,8 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
} }
rbac_perms = { rbac_perms = {
'get_once_secret': 'accounts.change_integrationapplication', 'get_once_secret': 'accounts.change_integrationapplication',
'get_account_secret': 'accounts.view_integrationapplication' 'get_account_secret': 'accounts.view_integrationapplication',
'get_sdks_info': 'accounts.view_integrationapplication'
} }
def read_file(self, path): def read_file(self, path):
@@ -36,7 +37,6 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
@action( @action(
['GET'], detail=False, url_path='sdks', ['GET'], detail=False, url_path='sdks',
permission_classes=[IsValidUser]
) )
def get_sdks_info(self, request, *args, **kwargs): def get_sdks_info(self, request, *args, **kwargs):
code_suffix_mapper = { code_suffix_mapper = {

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

@@ -12,6 +12,8 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
filterset_fields = ('alias',) filterset_fields = ('alias',)
def get_queryset(self): def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return VirtualAccount.objects.none()
return VirtualAccount.get_or_init_queryset() return VirtualAccount.get_or_init_queryset()
def get_object(self, ): def get_object(self, ):

View File

@@ -41,6 +41,7 @@ class AutomationAssetsListApi(generics.ListAPIView):
class AutomationRemoveAssetApi(generics.UpdateAPIView): class AutomationRemoveAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch'] http_method_names = ['patch']
@@ -59,6 +60,7 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
class AutomationAddAssetApi(generics.UpdateAPIView): class AutomationAddAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch'] http_method_names = ['patch']

View File

@@ -6,10 +6,13 @@ 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
@@ -23,7 +26,7 @@ __all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi', 'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi', 'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
'ChangSecretNodeAddRemoveApi' 'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet'
] ]
@@ -94,12 +97,13 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
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)
@@ -150,7 +154,27 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
model = ChangeSecretAutomation model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateAssetSerializer serializer_class = serializers.ChangeSecretUpdateAssetSerializer
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

@@ -62,7 +62,8 @@ class ChangeSecretDashboardApi(APIView):
status_counts = defaultdict(lambda: defaultdict(int)) status_counts = defaultdict(lambda: defaultdict(int))
for date_finished, status in results: for date_finished, status in results:
date_str = str(date_finished.date()) dt_local = timezone.localtime(date_finished)
date_str = str(dt_local.date())
if status == ChangeSecretRecordStatusChoice.failed: if status == ChangeSecretRecordStatusChoice.failed:
status_counts[date_str]['failed'] += 1 status_counts[date_str]['failed'] += 1
elif status == ChangeSecretRecordStatusChoice.success: elif status == ChangeSecretRecordStatusChoice.success:
@@ -90,10 +91,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
@@ -150,6 +150,9 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
http_method_names = ['get', 'options'] http_method_names = ['get', 'options']
def get_queryset(self): def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return CheckAccountEngine.objects.none()
return CheckAccountEngine.get_default_engines() return CheckAccountEngine.get_default_engines()
def filter_queryset(self, queryset: list): def filter_queryset(self, queryset: list):

View File

@@ -63,12 +63,10 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountAddAssetApi(AutomationAddAssetApi): class PushAccountAddAssetApi(AutomationAddAssetApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi): class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateNodeSerializer serializer_class = serializers.PushAccountUpdateNodeSerializer

View File

@@ -235,8 +235,8 @@ class AccountBackupHandler:
except Exception as e: except Exception as e:
error = str(e) error = str(e)
print(f'\033[31m>>> {error}\033[0m') print(f'\033[31m>>> {error}\033[0m')
self.execution.status = Status.error self.manager.status = Status.error
self.execution.summary['error'] = error self.manager.summary['error'] = error
def backup_by_obj_storage(self): def backup_by_obj_storage(self):
object_id = self.execution.snapshot.get('id') object_id = self.execution.snapshot.get('id')

View File

@@ -5,9 +5,10 @@ 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_atomic_db_connection from common.db.utils import safe_atomic_db_connection
@@ -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
@@ -112,10 +113,25 @@ 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) inventory_hosts = []
if asset.type == HostTypes.WINDOWS:
if self.secret_type == SecretType.SSH_KEY:
host['error'] = _("Windows does not support SSH key authentication")
return host
new_secret = self.get_secret(account)
if '>' in new_secret or '^' in new_secret:
host['error'] = _("Windows password cannot contain special characters like > ^")
return host
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}')
@@ -124,39 +140,53 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if asset.type == HostTypes.WINDOWS: if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD) accounts = accounts.filter(secret_type=SecretType.PASSWORD)
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
print(f'Windows {asset} does not support ssh key push')
return inventory_hosts
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
@@ -172,16 +202,17 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
with safe_atomic_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
@@ -192,12 +223,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_atomic_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

@@ -5,12 +5,14 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
register: db_info register: db_info
@@ -23,45 +25,53 @@
var: info var: info
- name: Check whether SQLServer User exist - name: Check whether SQLServer User exist
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded when: db_info is succeeded
register: user_exist register: user_exist
- name: Change SQLServer password - name: Change SQLServer password
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version" script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length != 0 when: user_exist.query_results[0] | length != 0
- name: Add SQLServer user - name: Add SQLServer user
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version" script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length == 0 when: user_exist.query_results[0] | length == 0
- name: Verify password - name: Verify password
community.general.mssql_script: mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
when: check_conn_after_change when: check_conn_after_change

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@@ -28,6 +28,12 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -61,6 +67,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -86,6 +97,11 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@@ -30,6 +30,12 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -63,6 +69,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -88,6 +99,11 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

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

@@ -9,7 +9,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 }}"
@@ -25,3 +25,8 @@ 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

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

@@ -9,7 +9,7 @@ from accounts.const import (
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
) )
from accounts.models import ChangeSecretRecord from accounts.models import ChangeSecretRecord
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg from accounts.notifications import ChangeSecretExecutionTaskMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer from accounts.serializers import ChangeSecretRecordBackUpSerializer
from common.utils import get_logger from common.utils import get_logger
from common.utils.file import encrypt_and_compress_zip_file from common.utils.file import encrypt_and_compress_zip_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
@@ -94,21 +94,17 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if not recipients: if not recipients:
return return
context = self.get_report_context() if not records:
for user in recipients:
ChangeSecretReportMsg(user, context).publish()
if not recorders:
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 +117,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

@@ -15,11 +15,13 @@ from common.decorators import bulk_create_decorator, bulk_update_decorator
from settings.models import LeakPasswords 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
@@ -217,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()
@@ -235,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)
@@ -264,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

@@ -5,12 +5,14 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT SELECT
l.name, l.name,

View File

@@ -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,
@@ -388,6 +390,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
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
need_analyser_gather_account.append((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: for analysis_data in need_analyser_gather_account:
@@ -403,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

@@ -5,12 +5,14 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
register: db_info register: db_info
@@ -23,47 +25,55 @@
var: info var: info
- name: Check whether SQLServer User exist - name: Check whether SQLServer User exist
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded when: db_info is succeeded
register: user_exist register: user_exist
- name: Change SQLServer password - name: Change SQLServer password
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version" script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length != 0 when: user_exist.query_results[0] | length != 0
register: change_info register: change_info
- name: Add SQLServer user - name: Add SQLServer user
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version" script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length == 0 when: user_exist.query_results[0] | length == 0
register: change_info register: change_info
- name: Verify password - name: Verify password
community.general.mssql_script: mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
when: check_conn_after_change when: check_conn_after_change

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@@ -28,6 +28,12 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -61,6 +67,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -86,6 +97,11 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@@ -30,6 +30,12 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -63,6 +69,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -84,9 +95,14 @@ i18n:
en: 'Home' en: 'Home'
Params groups label: Params groups label:
zh: '用户组' zh: '附加组'
ja: 'グループ' ja: '追加グループ'
en: 'Groups' en: 'Additional Group'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'

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

@@ -9,7 +9,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 }}"
@@ -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

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

@@ -5,11 +5,13 @@
tasks: tasks:
- name: "Remove account" - name: "Remove account"
community.general.mssql_script: mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
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 }}"
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "DROP LOGIN {{ account.username }}; select @@version" script: "DROP LOGIN {{ account.username }}; select @@version"

View File

@@ -3,7 +3,7 @@
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)

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

@@ -5,11 +5,13 @@
tasks: tasks:
- name: Verify account - name: Verify account
community.general.mssql_script: mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
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 }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version

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

@@ -9,3 +9,4 @@
vars: vars:
ansible_user: "{{ account.full_username }}" ansible_user: "{{ account.full_username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
ansible_timeout: 30

View File

@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from common.utils import get_logger from common.utils import get_logger
@@ -14,6 +11,9 @@ __all__ = ['AZUREVaultClient']
class AZUREVaultClient(object): class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret): def __init__(self, vault_url, tenant_id, client_id, client_secret):
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
authentication_endpoint = 'https://login.microsoftonline.com/' \ authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/' if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
@@ -23,6 +23,8 @@ class AZUREVaultClient(object):
self.client = SecretClient(vault_url=vault_url, credential=credentials) self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self): def is_active(self):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try: try:
self.client.set_secret('jumpserver', '666') self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e: except (ResourceNotFoundError, ClientAuthenticationError) as e:
@@ -32,6 +34,8 @@ class AZUREVaultClient(object):
return True, '' return True, ''
def get(self, name, version=None): def get(self, name, version=None):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try: try:
secret = self.client.get_secret(name, version) secret = self.client.get_secret(name, version)
return secret.value return secret.value

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

@@ -17,6 +17,7 @@ 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__) logger = get_logger(__file__)
@@ -233,7 +234,7 @@ 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, UUIDFilterMixin, BaseFilterSet): class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet):
@@ -242,3 +243,25 @@ class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterS
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(

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):
@@ -130,7 +132,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.platform return self.asset.platform
@lazyproperty @lazyproperty
def alias(self): def alias(self) -> str:
""" """
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id 别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
""" """
@@ -138,13 +140,13 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.username return self.username
return str(self.id) return str(self.id)
def is_virtual(self): def is_virtual(self) -> bool:
""" """
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名, 不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
""" """
return self.alias.startswith('@') return self.alias.startswith('@')
def is_ds_account(self): def is_ds_account(self) -> bool:
if self.is_virtual(): if self.is_virtual():
return '' return ''
if not self.asset.is_directory_service: if not self.asset.is_directory_service:
@@ -158,7 +160,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.ds return self.asset.ds
@lazyproperty @lazyproperty
def ds_domain(self): def ds_domain(self) -> str:
"""这个不能去掉perm_account 会动态设置这个值,以更改 full_username""" """这个不能去掉perm_account 会动态设置这个值,以更改 full_username"""
if self.is_virtual(): if self.is_virtual():
return '' return ''
@@ -170,17 +172,17 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return '@' in self.username or '\\' in self.username return '@' in self.username or '\\' in self.username
@property @property
def full_username(self): def full_username(self) -> str:
if not self.username_has_domain() and self.ds_domain: if not self.username_has_domain() and self.ds_domain:
return '{}@{}'.format(self.username, self.ds_domain) return '{}@{}'.format(self.username, self.ds_domain)
return self.username return self.username
@lazyproperty @lazyproperty
def has_secret(self): def has_secret(self) -> bool:
return bool(self.secret) return bool(self.secret)
@lazyproperty @lazyproperty
def versions(self): def versions(self) -> int:
return self.history.count() return self.history.count()
def get_su_from_accounts(self): def get_su_from_accounts(self):

View File

@@ -33,7 +33,7 @@ class IntegrationApplication(JMSOrgBaseModel):
return qs.filter(*query) return qs.filter(*query)
@property @property
def accounts_amount(self): def accounts_amount(self) -> int:
return self.get_accounts().count() return self.get_accounts().count()
@property @property

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

@@ -75,11 +75,11 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return bool(self.secret) return bool(self.secret)
@property @property
def has_username(self): def has_username(self) -> bool:
return bool(self.username) return bool(self.username)
@property @property
def spec_info(self): def spec_info(self) -> dict:
data = {} data = {}
if self.secret_type != SecretType.SSH_KEY: if self.secret_type != SecretType.SSH_KEY:
return data return data
@@ -87,13 +87,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return data return data
@property @property
def password(self): def password(self) -> str:
if self.secret_type == SecretType.PASSWORD: if self.secret_type == SecretType.PASSWORD:
return self.secret return self.secret
return None return None
@property @property
def private_key(self): def private_key(self) -> str:
if self.secret_type == SecretType.SSH_KEY: if self.secret_type == SecretType.SSH_KEY:
return self.secret return self.secret
return None return None
@@ -110,7 +110,7 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return None return None
@property @property
def ssh_key_fingerprint(self): def ssh_key_fingerprint(self) -> str:
if self.public_key: if self.public_key:
public_key = self.public_key public_key = self.public_key
elif self.private_key: elif self.private_key:

View File

@@ -56,7 +56,7 @@ class VaultModelMixin(models.Model):
__secret = None __secret = None
@property @property
def secret(self): def secret(self) -> str:
if self.__secret: if self.__secret:
return self.__secret return self.__secret
from accounts.backends import vault_client from accounts.backends import vault_client

View File

@@ -18,11 +18,11 @@ class VirtualAccount(JMSOrgBaseModel):
verbose_name = _('Virtual account') verbose_name = _('Virtual account')
@property @property
def name(self): def name(self) -> str:
return self.get_alias_display() return self.get_alias_display()
@property @property
def username(self): def username(self) -> str:
usernames_map = { usernames_map = {
AliasAccount.INPUT: _("Manual input"), AliasAccount.INPUT: _("Manual input"),
AliasAccount.USER: _("Same with user"), AliasAccount.USER: _("Same with user"),
@@ -32,7 +32,7 @@ class VirtualAccount(JMSOrgBaseModel):
return usernames_map.get(self.alias, '') return usernames_map.get(self.alias, '')
@property @property
def comment(self): def comment(self) -> str:
comments_map = { comments_map = {
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'), AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
AliasAccount.USER: _('The account username name same with user on connect'), AliasAccount.USER: _('The account username name same with user on connect'),

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

@@ -14,7 +14,7 @@ from accounts.models import Account, AccountTemplate, GatheredAccount
from accounts.tasks import push_accounts_to_assets_task from accounts.tasks import push_accounts_to_assets_task
from assets.const import Category, AllTypes from assets.const import Category, AllTypes
from assets.models import Asset from assets.models import Asset
from common.serializers import SecretReadableMixin from common.serializers import SecretReadableMixin, CommonBulkModelSerializer
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAccountSerializer, AuthValidateMixin from .base import BaseAccountSerializer, AuthValidateMixin
@@ -246,12 +246,15 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'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},
'source_id': {'required': False, 'allow_null': True}, 'source_id': {'required': False, 'allow_null': True},
} }
fields_unimport_template = ['params'] fields_unimport_template = ['params']
# 手动判断唯一性校验
validators = []
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
@@ -262,38 +265,53 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
) )
return queryset return queryset
def validate(self, attrs):
instance = getattr(self, "instance", None)
if instance:
return super().validate(attrs)
field_errors = {}
for _fields in Account._meta.unique_together:
lookup = {field: attrs.get(field) for field in _fields}
if Account.objects.filter(**lookup).exists():
verbose_names = ', '.join([str(Account._meta.get_field(f).verbose_name) for f in _fields])
msg_template = _('Account already exists. Field(s): {fields} must be unique.')
field_errors[_fields[0]] = msg_template.format(fields=verbose_names)
raise serializers.ValidationError(field_errors)
return attrs
class AccountDetailSerializer(AccountSerializer): class AccountDetailSerializer(AccountSerializer):
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
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']
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer): class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
asset = serializers.CharField(read_only=True, label=_('Asset')) asset = serializers.CharField(read_only=True, label=_('Asset'))
account = serializers.CharField(read_only=True, label=_('Account'))
state = serializers.CharField(read_only=True, label=_('State')) state = serializers.CharField(read_only=True, label=_('State'))
error = serializers.CharField(read_only=True, label=_('Error')) error = serializers.CharField(read_only=True, label=_('Error'))
changed = serializers.BooleanField(read_only=True, label=_('Changed')) changed = serializers.BooleanField(read_only=True, label=_('Changed'))
class AssetAccountBulkSerializer( class AssetAccountBulkSerializer(
AccountCreateUpdateSerializerMixin, AuthValidateMixin, serializers.ModelSerializer AccountCreateUpdateSerializerMixin, AuthValidateMixin, CommonBulkModelSerializer
): ):
su_from_username = serializers.CharField( su_from_username = serializers.CharField(
max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"), max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"),
allow_blank=True, allow_blank=True,
) )
assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'name', 'username', 'secret', 'secret_type', 'passphrase', 'name', 'username', 'secret', 'secret_type', 'secret_reset',
'privileged', 'is_active', 'comment', 'template', 'passphrase', 'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'params', 'assets', 'on_invalid', 'push_now', 'params',
'su_from_username', 'source', 'source_id', 'su_from_username', 'source', 'source_id',
] ]
extra_kwargs = { extra_kwargs = {
@@ -375,8 +393,7 @@ class AssetAccountBulkSerializer(
handler = self._handle_err_create handler = self._handle_err_create
return handler return handler
def perform_bulk_create(self, vd): def perform_bulk_create(self, vd, assets):
assets = vd.pop('assets')
on_invalid = vd.pop('on_invalid', 'skip') on_invalid = vd.pop('on_invalid', 'skip')
secret_type = vd.get('secret_type', 'password') secret_type = vd.get('secret_type', 'password')
@@ -384,8 +401,7 @@ class AssetAccountBulkSerializer(
vd['name'] = vd.get('username') vd['name'] = vd.get('username')
create_handler = self.get_create_handler(on_invalid) create_handler = self.get_create_handler(on_invalid)
asset_ids = [asset.id for asset in assets] secret_type_supports = Asset.get_secret_type_assets(assets, secret_type)
secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
_results = {} _results = {}
for asset in assets: for asset in assets:
@@ -393,6 +409,7 @@ class AssetAccountBulkSerializer(
_results[asset] = { _results[asset] = {
'error': _('Asset does not support this secret type: %s') % secret_type, 'error': _('Asset does not support this secret type: %s') % secret_type,
'state': 'error', 'state': 'error',
'account': vd['name'],
} }
continue continue
@@ -402,13 +419,13 @@ class AssetAccountBulkSerializer(
self.clean_auth_fields(vd) self.clean_auth_fields(vd)
instance, changed, state = self.perform_create(vd, create_handler) instance, changed, state = self.perform_create(vd, create_handler)
_results[asset] = { _results[asset] = {
'changed': changed, 'instance': instance.id, 'state': state 'changed': changed, 'instance': instance.id, 'state': state, 'account': vd['name']
} }
except serializers.ValidationError as e: except serializers.ValidationError as e:
_results[asset] = {'error': e.detail[0], 'state': 'error'} _results[asset] = {'error': e.detail[0], 'state': 'error', 'account': vd['name']}
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
_results[asset] = {'error': str(e), 'state': 'error'} _results[asset] = {'error': str(e), 'state': 'error', 'account': vd['name']}
results = [{'asset': asset, **result} for asset, result in _results.items()] results = [{'asset': asset, **result} for asset, result in _results.items()]
state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0} state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
@@ -425,7 +442,8 @@ class AssetAccountBulkSerializer(
errors.append({ errors.append({
'error': _('Account has exist'), 'error': _('Account has exist'),
'state': 'error', 'state': 'error',
'asset': str(result['asset']) 'asset': str(result['asset']),
'account': result.get('account'),
}) })
if errors: if errors:
raise serializers.ValidationError(errors) raise serializers.ValidationError(errors)
@@ -444,10 +462,16 @@ class AssetAccountBulkSerializer(
account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)] account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
push_accounts_to_assets_task.delay(account_ids, params) push_accounts_to_assets_task.delay(account_ids, params)
def create(self, validated_data): def bulk_create(self, validated_data, assets):
if not assets:
raise serializers.ValidationError(
{'assets': _('At least one asset or node must be specified')},
{'nodes': _('At least one asset or node must be specified')}
)
params = validated_data.pop('params', None) params = validated_data.pop('params', None)
push_now = validated_data.pop('push_now', False) push_now = validated_data.pop('push_now', False)
results = self.perform_bulk_create(validated_data) results = self.perform_bulk_create(validated_data, assets)
self.push_accounts_if_need(results, push_now, params) self.push_accounts_if_need(results, push_now, params)
for res in results: for res in results:
res['asset'] = str(res['asset']) res['asset'] = str(res['asset'])
@@ -455,6 +479,8 @@ class AssetAccountBulkSerializer(
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
fields = AccountSerializer.Meta.fields + ['spec_info'] fields = AccountSerializer.Meta.fields + ['spec_info']
extra_kwargs = { extra_kwargs = {
@@ -469,6 +495,7 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountHistorySerializer(serializers.ModelSerializer): class AccountHistorySerializer(serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
secret = serializers.CharField(label=_('Secret'), read_only=True)
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True) id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
class Meta: class Meta:

View File

@@ -70,12 +70,14 @@ class AuthValidateMixin(serializers.Serializer):
class BaseAccountSerializer( class BaseAccountSerializer(
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
): ):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta: class Meta:
model = BaseAccount model = BaseAccount
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

@@ -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'
] ]
@@ -128,7 +130,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_is_success(obj): def get_is_success(obj) -> bool:
return obj.status == ChangeSecretRecordStatusChoice.success return obj.status == ChangeSecretRecordStatusChoice.success
@@ -155,7 +157,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_asset(instance): def get_asset(instance) -> str:
return str(instance.asset) return str(instance.asset)
@staticmethod @staticmethod
@@ -163,7 +165,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
return str(instance.account) return str(instance.account)
@staticmethod @staticmethod
def get_is_success(obj): def get_is_success(obj) -> str:
if obj.status == ChangeSecretRecordStatusChoice.success.value: if obj.status == ChangeSecretRecordStatusChoice.success.value:
return _("Success") return _("Success")
return _("Failed") return _("Failed")
@@ -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) -> dict:
return account_secret_task_status.get(str(obj.id))
@staticmethod
def get_ttl(obj) -> int:
return account_secret_task_status.get_ttl(str(obj.id))

View File

@@ -69,7 +69,7 @@ class AssetRiskSerializer(serializers.Serializer):
risk_summary = serializers.SerializerMethodField() risk_summary = serializers.SerializerMethodField()
@staticmethod @staticmethod
def get_risk_summary(obj): def get_risk_summary(obj) -> dict:
summary = {} summary = {}
for risk in RiskChoice.choices: for risk in RiskChoice.choices:
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0) summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)

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(

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

@@ -1,36 +0,0 @@
{% load i18n %}
<h3>{% trans 'Task name' %}: {{ name }}</h3>
<h3>{% trans 'Task execution id' %}: {{ execution_id }}</h3>
<p>{% trans 'Respectful' %} {{ recipient }}</p>
<p>{% trans 'Hello! The following is the failure of changing the password of your assets or pushing the account. Please check and handle it in time.' %}</p>
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
<caption></caption>
<thead>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Account' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Error' %}</th>
</tr>
</thead>
<tbody>
{% for asset_name, account_username, error in asset_account_errors %}
<tr>
<td style="border: 1px solid #ddd; padding: 10px;">{{ asset_name }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ account_username }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">
<div style="
max-width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;"
title="{{ error }}"
>
{{ error }}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

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

@@ -3,3 +3,4 @@ from .connect_method import *
from .login_acl import * from .login_acl import *
from .login_asset_acl import * from .login_asset_acl import *
from .login_asset_check import * from .login_asset_check import *
from .data_masking import *

View File

@@ -0,0 +1,20 @@
from orgs.mixins.api import OrgBulkModelViewSet
from .common import ACLUserFilterMixin
from ..models import DataMaskingRule
from .. import serializers
__all__ = ['DataMaskingRuleViewSet']
class DataMaskingRuleFilter(ACLUserFilterMixin):
class Meta:
model = DataMaskingRule
fields = ('name',)
class DataMaskingRuleViewSet(OrgBulkModelViewSet):
model = DataMaskingRule
filterset_class = DataMaskingRuleFilter
search_fields = ('name',)
serializer_class = serializers.DataMaskingRuleSerializer

View File

@@ -8,7 +8,7 @@ __all__ = ['LoginAssetACLViewSet']
class LoginAssetACLFilter(ACLUserAssetFilterMixin): class LoginAssetACLFilter(ACLUserAssetFilterMixin):
class Meta: class Meta:
model = models.LoginAssetACL model = models.LoginAssetACL
fields = ['name', ] fields = ['name', 'action']
class LoginAssetACLViewSet(OrgBulkModelViewSet): class LoginAssetACLViewSet(OrgBulkModelViewSet):

View File

@@ -9,5 +9,6 @@ class ActionChoices(models.TextChoices):
warning = 'warning', _('Warn') warning = 'warning', _('Warn')
notice = 'notice', _('Notify') notice = 'notice', _('Notify')
notify_and_warn = 'notify_and_warn', _('Prompt 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

@@ -0,0 +1,45 @@
# Generated by Django 4.1.13 on 2025-10-07 16:16
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0002_auto_20210926_1047'),
]
operations = [
migrations.CreateModel(
name='DataMaskingRule',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('action', models.CharField(default='reject', max_length=64, verbose_name='Action')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('users', common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users')),
('assets', common.db.fields.JSONManyToManyField(default=dict, to='assets.Asset', verbose_name='Assets')),
('accounts', models.JSONField(default=list, verbose_name='Accounts')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('fields_pattern', models.CharField(default='password', max_length=128, verbose_name='Fields pattern')),
('masking_method', models.CharField(choices=[('fixed_char', 'Fixed Character Replacement'), ('hide_middle', 'Hide Middle Characters'), ('keep_prefix', 'Keep Prefix Only'), ('keep_suffix', 'Keep Suffix Only')], default='fixed_char', max_length=32, verbose_name='Masking Method')),
('mask_pattern', models.CharField(blank=True, default='######', max_length=128, null=True, verbose_name='Mask Pattern')),
('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
],
options={
'verbose_name': 'Data Masking Rule',
'unique_together': {('org_id', 'name')},
},
),
]

View File

@@ -2,3 +2,4 @@ from .command_acl import *
from .connect_method import * from .connect_method import *
from .login_acl import * from .login_acl import *
from .login_asset_acl import * from .login_asset_acl import *
from .data_masking import *

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

@@ -0,0 +1,42 @@
from django.db import models
from acls.models import UserAssetAccountBaseACL
from common.utils import get_logger
from django.utils.translation import gettext_lazy as _
logger = get_logger(__file__)
__all__ = ['MaskingMethod', 'DataMaskingRule']
class MaskingMethod(models.TextChoices):
fixed_char = "fixed_char", _("Fixed Character Replacement") # 固定字符替换
hide_middle = "hide_middle", _("Hide Middle Characters") # 隐藏中间几位
keep_prefix = "keep_prefix", _("Keep Prefix Only") # 只保留前缀
keep_suffix = "keep_suffix", _("Keep Suffix Only") # 只保留后缀
class DataMaskingRule(UserAssetAccountBaseACL):
name = models.CharField(max_length=128, verbose_name=_("Name"))
fields_pattern = models.CharField(max_length=128, default='password', verbose_name=_("Fields pattern"))
masking_method = models.CharField(
max_length=32,
choices=MaskingMethod.choices,
default=MaskingMethod.fixed_char,
verbose_name=_("Masking Method"),
)
mask_pattern = models.CharField(
max_length=128,
verbose_name=_("Mask Pattern"),
default="######",
blank=True,
null=True,
)
def __str__(self):
return self.name
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Data Masking Rule")

View File

@@ -1,30 +1,52 @@
from django.template.loader import render_to_string from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.models import Account from accounts.models import Account
from acls.models import LoginACL, LoginAssetACL
from assets.models import Asset from assets.models import Asset
from audits.models import UserLoginLog from audits.models import UserLoginLog
from common.views.template import custom_render_to_string
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from users.models import User from users.models import User
class UserLoginReminderMsg(UserMessage): class UserLoginReminderMsg(UserMessage):
subject = _('User login reminder') subject = _('User login reminder')
template_name = 'acls/user_login_reminder.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "recipient_name", "label": _("Recipient name"), "default": "John"},
{"name": "recipient_username", "label": _("Recipient username"), "default": "john"},
{"name": "user_agent", "label": _('User agent'), "default": "Mozilla/5.0"},
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
{"name": "login_from", "label": _('Login from'), "default": "web"},
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"},
]
def __init__(self, user, user_log: UserLoginLog): def __init__(self, user, user_log: UserLoginLog, acl: LoginACL):
self.user_log = user_log self.user_log = user_log
self.acl_name = str(acl)
self.login_from = user_log.get_type_display()
now = timezone.localtime(user_log.datetime)
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
super().__init__(user) super().__init__(user)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
user_log = self.user_log user_log = self.user_log
context = { context = {
'ip': user_log.ip, 'ip': user_log.ip,
'time': self.time,
'city': user_log.city, 'city': user_log.city,
'acl_name': self.acl_name,
'login_from': self.login_from,
'username': user_log.username, 'username': user_log.username,
'recipient': self.user, 'recipient_name': self.user.name,
'recipient_username': self.user.username,
'user_agent': user_log.user_agent, 'user_agent': user_log.user_agent,
} }
message = render_to_string('acls/user_login_reminder.html', context) message = custom_render_to_string(self.template_name, context)
return { return {
'subject': str(self.subject), 'subject': str(self.subject),
@@ -40,24 +62,55 @@ class UserLoginReminderMsg(UserMessage):
class AssetLoginReminderMsg(UserMessage): class AssetLoginReminderMsg(UserMessage):
subject = _('User login alert for asset') subject = _('User login alert for asset')
template_name = 'acls/asset_login_reminder.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "asset", "label": _('Asset'), "default": "dev server"},
{"name": "recipient_name", "label": _('Recipient name'), "default": "John"},
{"name": "recipient_username", "label": _('Recipient username'), "default": "john"},
{"name": "account", "label": _('Account Input username'), "default": "root"},
{"name": "account_name", "label": _('Account name'), "default": "root"},
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "login_from", "label": _('Login from'), "default": "web"},
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"}
]
def __init__(self, user, asset: Asset, login_user: User, account: Account, input_username): def __init__(
self, user, asset: Asset, login_user: User,
account: Account, acl: LoginAssetACL,
ip, input_username, login_from
):
self.ip = ip
self.asset = asset self.asset = asset
self.login_user = login_user self.login_user = login_user
self.account = account self.account = account
self.acl_name = str(acl)
self.login_from = login_from
self.login_user = login_user
self.input_username = input_username self.input_username = input_username
now = timezone.localtime(timezone.now())
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
super().__init__(user) super().__init__(user)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
context = { context = {
'recipient': self.user, 'ip': self.ip,
'time': self.time,
'login_from': self.login_from,
'recipient_name': self.user.name,
'recipient_username': self.user.username,
'username': self.login_user.username, 'username': self.login_user.username,
'name': self.login_user.name, 'name': self.login_user.name,
'asset': str(self.asset), 'asset': str(self.asset),
'account': self.input_username, 'account': self.input_username,
'account_name': self.account.name, 'account_name': self.account.name,
'acl_name': self.acl_name,
} }
message = render_to_string('acls/asset_login_reminder.html', context) message = custom_render_to_string(self.template_name, context)
return { return {
'subject': str(self.subject), 'subject': str(self.subject),

View File

@@ -3,3 +3,4 @@ from .connect_method import *
from .login_acl import * from .login_acl import *
from .login_asset_acl import * from .login_asset_acl import *
from .login_asset_check import * from .login_asset_check import *
from .data_masking import *

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):
@@ -88,7 +90,7 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
fields_small = fields_mini + [ fields_small = fields_mini + [
"is_active", "priority", "action", "is_active", "priority", "action",
"date_created", "date_updated", "date_created", "date_updated",
"comment", "created_by", "org_id", "comment", "created_by"
] ]
fields_m2m = ["reviewers", ] fields_m2m = ["reviewers", ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
@@ -98,6 +100,20 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
'reviewers': {'label': _('Recipients')}, 'reviewers': {'label': _('Recipients')},
} }
class BaseUserACLSerializer(BaseACLSerializer):
users = JSONManyToManyField(label=_('User'))
class Meta(BaseACLSerializer.Meta):
fields = BaseACLSerializer.Meta.fields + ['users']
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
assets = JSONManyToManyField(label=_('Asset'))
accounts = serializers.ListField(label=_('Account'))
class Meta(BaseUserACLSerializer.Meta):
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts', 'org_id']
def validate_reviewers(self, reviewers): def validate_reviewers(self, reviewers):
action = self.initial_data.get('action') action = self.initial_data.get('action')
if not action and self.instance: if not action and self.instance:
@@ -116,19 +132,4 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
"None of the reviewers belong to Organization `{}`".format(org.name) "None of the reviewers belong to Organization `{}`".format(org.name)
) )
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return valid_reviewers return valid_reviewers
class BaseUserACLSerializer(BaseACLSerializer):
users = JSONManyToManyField(label=_('User'))
class Meta(BaseACLSerializer.Meta):
fields = BaseACLSerializer.Meta.fields + ['users']
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
assets = JSONManyToManyField(label=_('Asset'))
accounts = serializers.ListField(label=_('Account'))
class Meta(BaseUserACLSerializer.Meta):
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts']

View File

@@ -33,7 +33,10 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
model = CommandFilterACL model = CommandFilterACL
fields = BaseSerializer.Meta.fields + ['command_groups'] fields = BaseSerializer.Meta.fields + ['command_groups']
action_choices_exclude = [ action_choices_exclude = [
ActionChoices.notice, ActionChoices.face_verify, ActionChoices.face_online ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online,
ActionChoices.change_secret
] ]

View File

@@ -1,4 +1,4 @@
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.serializers.mixin import CommonBulkModelSerializer
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
from ..const import ActionChoices from ..const import ActionChoices
from ..models import ConnectMethodACL from ..models import ConnectMethodACL
@@ -6,14 +6,17 @@ from ..models import ConnectMethodACL
__all__ = ["ConnectMethodACLSerializer"] __all__ = ["ConnectMethodACLSerializer"]
class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer): class ConnectMethodACLSerializer(BaseSerializer, CommonBulkModelSerializer):
class Meta(BaseSerializer.Meta): class Meta(BaseSerializer.Meta):
model = ConnectMethodACL model = ConnectMethodACL
fields = [ fields = [
i for i in BaseSerializer.Meta.fields + ['connect_methods'] i for i in BaseSerializer.Meta.fields + ['connect_methods']
if i not in ['assets', 'accounts'] if i not in ['assets', 'accounts', 'org_id']
] ]
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.face_verify, ActionChoices.face_online ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online,
ActionChoices.change_secret
] ]

View File

@@ -0,0 +1,19 @@
from django.utils.translation import gettext_lazy as _
from acls.models import MaskingMethod, DataMaskingRule
from common.serializers.fields import LabeledChoiceField
from common.serializers.mixin import CommonBulkModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
__all__ = ['DataMaskingRuleSerializer']
class DataMaskingRuleSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
masking_method = LabeledChoiceField(
choices=MaskingMethod.choices, default=MaskingMethod.fixed_char, label=_('Masking Method')
)
class Meta(BaseSerializer.Meta):
model = DataMaskingRule
fields = BaseSerializer.Meta.fields + ['fields_pattern', 'masking_method', 'mask_pattern']

View File

@@ -1,7 +1,7 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from common.serializers import CommonBulkModelSerializer
from common.serializers import MethodSerializer from common.serializers import MethodSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserACLSerializer from .base import BaseUserACLSerializer
from .rules import RuleSerializer from .rules import RuleSerializer
from ..const import ActionChoices from ..const import ActionChoices
@@ -12,17 +12,18 @@ __all__ = ["LoginACLSerializer"]
common_help_text = _("With * indicating a match all. ") common_help_text = _("With * indicating a match all. ")
class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer): class LoginACLSerializer(BaseUserACLSerializer, CommonBulkModelSerializer):
rules = MethodSerializer(label=_('Rule')) rules = MethodSerializer(label=_('Rule'))
class Meta(BaseUserACLSerializer.Meta): class Meta(BaseUserACLSerializer.Meta):
model = LoginACL model = LoginACL
fields = BaseUserACLSerializer.Meta.fields + ['rules', ] fields = list((set(BaseUserACLSerializer.Meta.fields) | {'rules'}))
action_choices_exclude = [ action_choices_exclude = [
ActionChoices.warning, ActionChoices.warning,
ActionChoices.notify_and_warn, ActionChoices.notify_and_warn,
ActionChoices.face_online, ActionChoices.face_online,
ActionChoices.face_verify ActionChoices.face_verify,
ActionChoices.change_secret
] ]
def get_rules_serializer(self): def get_rules_serializer(self):

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,13 +1,17 @@
{% load i18n %} {% load i18n %}
<h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3> <h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3>
<hr> <hr>
<p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p> <p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p>
<p><strong>{% trans 'Asset details' %}:</strong></p> <p><strong>{% trans 'Asset details' %}:</strong></p>
<ul> <ul>
<li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li> <li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li>
<li><strong>IP:</strong> [{{ ip }}]</li>
<li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li> <li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li>
<li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li> <li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li>
<li><strong>{% trans 'Login asset acl' %}:</strong> [{{ acl_name }}]</li>
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
</ul> </ul>
<hr> <hr>

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