Compare commits

...

182 Commits

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

* perf: Optimization conditions

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-09-27 14:17:16 +08:00
wangruidong
0311446384 perf: playbook clone with file 2024-09-27 14:13:35 +08:00
feng
f7030e4fee perf: Login encryption key cache added 2024-09-26 15:11:35 +08:00
ZhaoJiSen
fce8cc375f Merge pull request #14230 from jumpserver/pr@dev@max_password_length
perf: The maximum length of the randomly generated password is changed to 36
2024-09-25 11:00:45 +08:00
feng
920199c6df perf: The maximum length of the randomly generated password is changed to 36 2024-09-25 10:52:16 +08:00
feng
d09eb3c4fa perf: Lock username is not case sensitive 2024-09-23 14:11:55 +08:00
ibuler
6e8affcdd6 perf: ops db migrate 2024-09-19 21:39:55 +08:00
老广
0b3a7bb020 Merge pull request #14203 from jumpserver/dev
merge: from dev to master
2024-09-19 19:37:19 +08:00
wangruidong
647736f4e3 fix: SAML2 500 error caused by duplicate email or username 2024-09-19 17:49:53 +08:00
ZhaoJiSen
cbc09d84df Merge pull request #14202 from jumpserver/pr@dev@password_rule
perf: Password rule import csv help_text
2024-09-19 16:54:52 +08:00
feng
4c957dd03b perf: Password rule import csv help_text 2024-09-19 16:51:43 +08:00
wangruidong
d34b65890f fix: import account failed 2024-09-19 15:12:05 +08:00
Bai
b53968ac00 delete: ansible log in logging.py 2024-09-19 15:11:31 +08:00
ZhaoJiSen
f2ccb15101 Merge pull request #14198 from jumpserver/pr@dev@saml
perf: Bind user group support str
2024-09-19 12:01:20 +08:00
feng
db5bf046fc perf: Bind user group support str 2024-09-19 11:58:45 +08:00
ibuler
59c87483e6 perf: filter gateway with new params 2024-09-19 11:33:02 +08:00
github-actions[bot]
26420b78f8 perf: Update Dockerfile with new base image tag 2024-09-19 11:18:04 +08:00
wangruidong
e47bdc093e perf: trigger core base image build 2024-09-19 11:18:04 +08:00
wangruidong
3dde80a60a fix: Password reset is only required for AUTH_BACKEND_MODEL 2024-09-19 11:08:11 +08:00
feng
e373a79d63 perf: Gateway type asset filter 2024-09-19 10:45:59 +08:00
wangruidong
744a5cd0e3 perf: Modify relative file path 2024-09-19 10:41:12 +08:00
wangruidong
37ca4a46ee perf: add clean_site_packages.sh file path to build-base-image.yml 2024-09-19 10:25:08 +08:00
wangruidong
0dc9214f98 fix: LDAP HA the login log did not record the authentication backend 2024-09-18 18:53:03 +08:00
wangruidong
513508654b fix: minio test failed 2024-09-18 18:51:25 +08:00
feng
ef2b12fa0f perf: Export template with prompts 2024-09-18 18:26:38 +08:00
feng
4e719ecacd perf: TimerExecution translate 2024-09-18 15:02:17 +08:00
ibuler
755a124b50 perf: checkout repo 2024-09-14 18:32:23 +08:00
zhaojisen
d6888776e7 perf: translate 2024-09-14 18:20:46 +08:00
wangruidong
29e233e715 perf: RemoteApp machine deployOption translate 2024-09-14 18:19:22 +08:00
wangruidong
99c3696d96 fix: Failed to import csv data 2024-09-14 18:17:15 +08:00
ibuler
ed6de83e8c perf: workflow push with full name 2024-09-14 18:09:30 +08:00
Eric
134f1a440c perf: replay part file download 2024-09-14 18:06:05 +08:00
ibuler
7da82242fe perf: github action workflow 2024-09-14 17:47:34 +08:00
Eric
2fd50d2425 perf: update compilemessages check ci 2024-09-14 17:38:07 +08:00
fit2bot
41a3e89248 chore: using pull pull request not push event (#14164)
* perf: diff with head not dev

* chore: using pull pull request not push event

---------

Co-authored-by: ibuler <ibuler@qq.com>
2024-09-14 17:21:49 +08:00
Bai
b125297c37 feat: GitHub Actions add compilemessages checked 2024-09-14 17:04:35 +08:00
ibuler
24255b69ee perf: diff with head not dev 2024-09-14 17:01:02 +08:00
ibuler
3bb51b39c4 perf: github action fetch branches 2024-09-14 16:51:25 +08:00
ibuler
b54da7d3b3 perf: workflow build base image 2024-09-14 16:42:06 +08:00
ibuler
534af0abf0 perf: build workflow 2024-09-14 16:32:15 +08:00
ibuler
8b0073333b perf: change git workflow 2024-09-14 16:25:06 +08:00
ZhaoJiSen
d8af2274f4 Merge pull request #14154 from jumpserver/pr@dev@koko_zh_translate
perf: KOKO zh translate
2024-09-14 15:34:27 +08:00
ibuler
3dd828d703 perf: workflow build base image 2024-09-14 15:34:20 +08:00
feng
fa6b4a5b63 perf: KOKO zh translate 2024-09-14 15:32:52 +08:00
ZhaoJiSen
8bd86c77f9 Merge pull request #14151 from jumpserver/pr@dev@win_rdp_ping
fix: Windows rdp ping fail
2024-09-14 14:48:17 +08:00
feng
3828e89cf8 fix: Windows rdp ping fail 2024-09-14 14:47:14 +08:00
wangruidong
e531b040ef fix: compilemessages error 2024-09-13 22:38:58 +08:00
wangruidong
3eee84a34e fix: delete ReplayStorage error 2024-09-13 19:09:21 +08:00
wangruidong
ab29df5991 fix: command search input error 2024-09-13 19:05:38 +08:00
wangruidong
b042f00688 fix: command search input error 2024-09-13 19:05:11 +08:00
github-actions[bot]
5beebaf51c perf: Update Dockerfile with new base image tag 2024-09-13 19:03:39 +08:00
wangruidong
50f075cc7e fix: Historical sessions download failed 2024-09-13 19:03:39 +08:00
Bai
e997236159 perf: Modify gunicorn log file rotate yesterday dir 2024-09-13 18:37:13 +08:00
ZhaoJiSen
c8b1d892e3 Merge pull request #14145 from jumpserver/pr@dev@account_already_exists
fix: Translate Account already exists
2024-09-13 18:33:25 +08:00
feng
9cb9e7328b fix: Translate Account already exists 2024-09-13 18:32:06 +08:00
feng
85129da942 perf: Postgresql add ssl mode 2024-09-13 17:49:14 +08:00
ZhaoJiSen
1cb00b1db4 Merge pull request #14138 from jumpserver/pr@dev@markdown_html
fix: Internal letter hyperlinks cannot be redirected
2024-09-13 11:07:08 +08:00
feng
c3798bfa95 fix: Internal letter hyperlinks cannot be redirected 2024-09-13 11:05:22 +08:00
github-actions[bot]
1d280599ae perf: Update Dockerfile with new base image tag 2024-09-12 18:55:01 +08:00
feng
ee8d7cdcac perf: Upgrade ansible postgresql 2024-09-12 18:55:01 +08:00
Aaron3S
1b4114fd5f perf: Optimize chen translation 2024-09-12 18:49:04 +08:00
Chenyang Shen
3c6c476f2e Merge pull request #14135 from jumpserver/pr@dev@feat_add_some_chen_i18n
feat: add chen i18n
2024-09-12 18:41:47 +08:00
Aaron3S
f19e3fedbd feat: add chen i18n 2024-09-12 18:38:16 +08:00
Bai
542e64278f perf: fix migrate adhoc playbook 2024-09-12 16:33:36 +08:00
wangruidong
cd76294e81 fix: migrate ops adhoc and playbook unique_together error 2024-09-12 16:33:36 +08:00
wangruidong
4f9158b2ad fix: ldap test config msg error 2024-09-12 10:41:17 +08:00
ZhaoJiSen
e319f20296 Merge pull request #14126 from jumpserver/pr@dev@third_party_user_login_failed
perf: Third-party user login failed
2024-09-12 10:23:05 +08:00
feng
b00f3a851c perf: Third-party user login failed 2024-09-12 10:19:40 +08:00
wangruidong
ab529fd22c fix: i18n compilemessages error 2024-09-12 10:07:12 +08:00
wangruidong
c2784c44ad feat: LDAP HA 2024-09-11 18:26:11 +08:00
feng
512e727ac6 feat: Postgresql support ssl 2024-09-11 18:12:25 +08:00
wangruidong
2dd0154967 perf: modify only_myself to only_mine 2024-09-11 18:02:35 +08:00
wangruidong
f55869a449 feat: Support playbook, adhoc share 2024-09-11 17:52:06 +08:00
wangruidong
b6f3c23787 perf: task description translate 2024-09-11 14:57:07 +08:00
github-actions[bot]
6982ab1efc perf: Update Dockerfile with new base image tag 2024-09-10 15:54:05 +08:00
wangruidong
db4d841bb0 perf: add xpack task description 2024-09-10 15:54:05 +08:00
wangruidong
ef91ebb468 perf: clean expired and account is null SyncInstanceTask record 2024-09-10 15:50:11 +08:00
fit2bot
6264319c51 perf: When connected through a gateway, you can use nc to forward data (#14110)
Co-authored-by: feng <1304903146@qq.com>
Co-authored-by: Bryan <jiangjie.bai@fit2cloud.com>
2024-09-09 19:00:48 +08:00
fit2bot
1417abecfb perf: Add task description (#14033)
Co-authored-by: ZhaoJiSen <97007455+ZhaoJiSen@users.noreply.github.com>
2024-09-09 18:54:33 +08:00
老广
bd548b3fe2 Revert "perf: update deps"
This reverts commit 76b6489636.
2024-09-09 15:33:57 +08:00
ZhaoJiSen
94cef9ea6e Merge pull request #14106 from jumpserver/pr@dev@translate
perf: Translate
2024-09-09 14:29:04 +08:00
feng626
a338613b5a Merge branch 'dev' into pr@dev@translate 2024-09-09 14:27:38 +08:00
feng
0d833a966c perf: Translate 2024-09-09 14:15:16 +08:00
ibuler
76b6489636 perf: update deps 2024-09-09 11:39:51 +08:00
fit2bot
763fe778d5 perf: finish this feat (#14079)
* perf: basic finished

* perf: finish this feat

* perf: add datetime demo

---------

Co-authored-by: ibuler <ibuler@qq.com>
2024-09-09 11:26:24 +08:00
ibuler
cf1dc79c68 perf: applet host tips 2024-09-09 10:25:01 +08:00
ibuler
7973239424 perf: support change gateway platform 2024-09-06 17:35:57 +08:00
feng
1baacd0b2c perf: Disable delete admin user 2024-09-06 15:37:12 +08:00
feng
054d385ffc perf: Acl action add notify and warn 2024-09-06 11:07:30 +08:00
wangruidong
50d3a4906a feat: Add announcement start and end dates 2024-09-06 10:54:27 +08:00
wangruidong
c8b7008d42 perf: Translate 2024-09-06 10:54:12 +08:00
kebyn
e94520a3fd fix: 修复非标准实现 X-Forwarded-For 时的问题 2024-09-06 10:31:58 +08:00
wangruidong
55e8e34226 fix: 500 error caused by duplicate email or username 2024-09-06 10:22:37 +08:00
wangruidong
8755ece633 perf: Translate 2024-09-05 19:24:19 +08:00
feng
c545e2a3aa perf: Support SAML2, OIDC user authentication services, mapping user group field information 2024-09-04 18:42:47 +08:00
wangruidong
1068662ab1 perf: Optimize asset connection speed with es command storage 2024-09-02 13:52:23 +08:00
ZhaoJiSen
75141741a1 Merge pull request #14062 from jumpserver/pr@dev@translate
perf: Translate
2024-08-30 15:18:45 +08:00
feng
9da507bb62 perf: Translate 2024-08-30 15:16:44 +08:00
fit2bot
160293365a perf: Regularly delete useless password change push records (#14026)
* perf: If the user Home page does not exist, push will fail

* perf: Change secret add uid parameter

* perf: Regularly delete useless password change push records

---------

Co-authored-by: feng <1304903146@qq.com>
2024-08-30 15:01:40 +08:00
wangruidong
7a19007aba perf: ldap import user error msg 2024-08-30 14:55:22 +08:00
ibuler
f866b93f96 perf: refresh oracle ports if need 2024-08-29 19:06:55 +08:00
feng
b9e64747ac perf: View the internal message and convert the content into markdown 2024-08-29 17:28:12 +08:00
fit2bot
25a473dc99 chore: update checkout action 2024-08-29 17:22:25 +08:00
Bai
e3bf015aa9 fix: user role can't open page of password & ssh key 2024-08-29 14:44:36 +08:00
Bryan
6d3d4a08af Update README.md (#14043)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md
2024-08-28 17:55:56 +08:00
Bai
9554de4ea6 fix: User check password need reset 2024-08-28 15:08:24 +08:00
ZhaoJiSen
6157ff7b7d Merge pull request #14032 from jumpserver/pr@dev@koko_translate
perf: Koko translate
2024-08-27 17:41:30 +08:00
feng
774fd176fd perf: Koko translate 2024-08-27 17:39:37 +08:00
ibuler
b489db8054 perf: add a script activate user manually 2024-08-21 14:55:57 +08:00
ZhaoJiSen
6b9fa6e01f Merge pull request #14010 from jumpserver/pr@dev@view_asset
perf: View asset mini api add domain platform field
2024-08-21 11:34:20 +08:00
ZhaoJiSen
9b59954393 Merge pull request #14011 from jumpserver/pr@dev@test_asset
perf: No permission to test asset connectivity
2024-08-21 11:33:55 +08:00
feng
ecaf19563f perf: No permission to test asset connectivity 2024-08-21 11:28:39 +08:00
feng
c431e96eaf perf: View asset mini api add domain platform field 2024-08-21 10:35:07 +08:00
ZhaoJiSen
d86f241450 Merge pull request #14005 from jumpserver/pr@dev@translate
perf: Translate
2024-08-20 11:30:37 +08:00
feng
3252db31fe perf: Translate 2024-08-20 11:28:30 +08:00
Bai
dac118dd26 perf: delete organization message 2024-08-19 16:06:37 +08:00
fit2bot
181eb621c0 perf: Remove kubernetes tree api (#13995)
* perf: Remove kubernetes tree api

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: feng <1304903146@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
2024-08-19 16:04:00 +08:00
ibuler
828582333d perf: remove ansible receptcel 2024-08-19 11:16:58 +08:00
github-actions[bot]
657f7f822b perf: Update Dockerfile with new base image tag 2024-08-19 10:50:39 +08:00
ibuler
93627e4f9d perf: clean site packages 2024-08-19 10:50:39 +08:00
feng
2adb2519fa perf: Push password change and select finally use rdp to test connectivity. rdp does not support gateway 2024-08-19 10:20:33 +08:00
Bryan
56373e362b Merge pull request #13988 from jumpserver/dev
v4.1.0
2024-08-16 18:40:35 +08:00
wangruidong
32ec48ac14 perf: Improve performance by optimizing ES index creation 2024-08-16 18:19:04 +08:00
Bryan
02fc045370 Merge pull request #13600 from jumpserver/dev
v4.0.0
2024-07-03 19:04:35 +08:00
Bryan
e4ac73896f Merge pull request #13452 from jumpserver/dev
v3.10.11-lts
2024-06-19 16:01:26 +08:00
Bryan
1518f792d6 Merge pull request #13236 from jumpserver/dev
v3.10.10-lts
2024-05-16 16:04:07 +08:00
Bai
67277dd622 fix: 修复仪表盘会话排序数量都是 1 的问题 2024-04-22 19:42:33 +08:00
Bryan
82e7f020ea Merge pull request #13094 from jumpserver/dev
v3.10.9 (dev to master)
2024-04-22 19:39:53 +08:00
Bryan
f20b9e01ab Merge pull request #13062 from jumpserver/dev
v3.10.8 dev to master
2024-04-18 18:01:20 +08:00
Bryan
8cf8a3701b Merge pull request #13059 from jumpserver/dev
v3.10.8
2024-04-18 17:16:37 +08:00
Bryan
7ba24293d1 Merge pull request #12736 from jumpserver/pr@dev@master_fix
fix: 解决冲突
2024-02-29 16:38:43 +08:00
Bai
f10114c9ed fix: 解决冲突 2024-02-29 16:37:10 +08:00
Bryan
cf31cbfb07 Merge pull request #12729 from jumpserver/dev
v3.10.4
2024-02-29 16:19:59 +08:00
wangruidong
0edad24d5d fix: 资产过期消息提示发送失败 2024-02-04 11:41:48 +08:00
ibuler
1f1c1a9157 fix: 修复定时检测用户是否活跃任务无法执行的问题 2024-01-23 09:28:38 +00:00
feng
6c9d271ae1 fix: redis 密码有特殊字符celery beat启动失败 2024-01-22 06:18:34 +00:00
Bai
6ff852e225 perf: 修复 Count 时没有去重的问题 2024-01-22 06:16:25 +00:00
Bryan
baa75dc735 Merge pull request #12566 from jumpserver/master
v3.10.2
2024-01-17 07:34:28 -04:00
Bryan
8a9f0436b8 Merge pull request #12565 from jumpserver/dev
v3.10.2
2024-01-17 07:23:30 -04:00
Bryan
a9620a3cbe Merge pull request #12461 from jumpserver/master
v3.10.1
2023-12-29 11:33:05 +05:00
Bryan
769e7dc8a0 Merge pull request #12460 from jumpserver/dev
v3.10.1
2023-12-29 11:20:36 +05:00
Bryan
2a70449411 Merge pull request #12458 from jumpserver/dev
v3.10.1
2023-12-29 11:01:13 +05:00
Bryan
8df720f19e Merge pull request #12401 from jumpserver/dev
v3.10
2023-12-21 15:14:19 +05:00
老广
dabbb45f6e Merge pull request #12144 from jumpserver/dev
v3.9.0
2023-11-16 18:23:05 +08:00
Bryan
ce24c1c3fd Merge pull request #11914 from jumpserver/dev
v3.8.0
2023-10-19 03:37:39 -05:00
Bryan
3c54c82ce9 Merge pull request #11636 from jumpserver/dev
v3.7.0
2023-09-21 17:02:48 +08:00
218 changed files with 8293 additions and 4238 deletions

View File

@@ -1,21 +1,33 @@
name: Build and Push Base Image
on:
push:
pull_request:
branches:
- 'pr*'
- 'dev'
- 'v*'
paths:
- 'poetry.lock'
- 'pyproject.toml'
- 'Dockerfile-base'
- poetry.lock
- pyproject.toml
- Dockerfile-base
- package.json
- go.mod
- yarn.lock
- pom.xml
- install_deps.sh
- utils/clean_site_packages.sh
types:
- opened
- synchronize
- reopened
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -55,6 +67,6 @@ jobs:
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add Dockerfile
git commit -m "perf: Update Dockerfile with new base image tag"
git push
git push origin ${{ github.event.pull_request.head.ref }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,31 @@
name: Check I18n files CompileMessages
on:
pull_request:
branches:
- 'dev'
paths:
- 'apps/i18n/core/**/*.po'
types:
- opened
- synchronize
- reopened
jobs:
compile-messages-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and check compilemessages
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: false
file: Dockerfile
target: stage-build
tags: jumpserver/core:stage-build

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20240808_054051 AS stage-build
FROM jumpserver/core-base:20240924_031841 AS stage-build
ARG VERSION

View File

@@ -43,12 +43,18 @@ RUN set -ex \
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
--mount=type=bind,source=poetry.lock,target=poetry.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install --only main
&& poetry install --only main \
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
&& bash clean_site_packages.sh

View File

@@ -32,6 +32,8 @@ Access JumpServer in your browser at `http://your-jumpserver-ip/`
- Username: `admin`
- Password: `ChangeMe`
[![JumpServer Quickstart](https://github.com/user-attachments/assets/0f32f52b-9935-485e-8534-336c63389612)](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
## Screenshots
<table style="border-collapse: collapse; border: 1px solid black;">

View File

@@ -4,6 +4,9 @@
ansible_python_interpreter: /opt/py3/bin/python
db_name: "{{ jms_asset.spec_info.db_name }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Test MySQL connection
@@ -13,9 +16,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version
register: db_info
@@ -30,9 +33,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
@@ -47,7 +50,7 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Test PostgreSQL connection
@@ -11,6 +15,10 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
register: result
failed_when: not result.is_available
@@ -28,6 +36,10 @@
db: "{{ jms_asset.spec_info.db_name }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
role_attr_flags: LOGIN
ignore_errors: true
when: result is succeeded
@@ -39,3 +51,7 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"

View File

@@ -14,27 +14,15 @@
- name: "Add {{ account.username }} user"
ansible.builtin.user:
name: "{{ account.username }}"
shell: "{{ params.shell }}"
home: "{{ params.home | default('/home/' + account.username, true) }}"
groups: "{{ params.groups }}"
uid: "{{ params.uid | int if params.uid | 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 }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1
state: present
when: user_info.failed
- name: "Add {{ account.username }} group"
ansible.builtin.group:
name: "{{ account.username }}"
state: present
when: user_info.failed
- name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when:
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
@@ -54,14 +42,40 @@
ignore_errors: true
when: account.secret_type == "password"
- name: remove jumpserver ssh key
- name: "Get home directory for {{ account.username }}"
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile:
dest: "{{ ssh_params.dest }}"
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
regexp: "{{ ssh_params.regexp }}"
state: absent
when:
- account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key:
@@ -79,7 +93,7 @@
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
become: "{{ account.become.ansible_become | default(False) }}"
become_method: su
become_user: "{{ account.become.ansible_user | default('') }}"
@@ -95,7 +109,7 @@
login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost

View File

@@ -34,6 +34,12 @@ params:
default: ''
help_text: "{{ 'Params groups help text' | trans }}"
- name: uid
type: str
label: "{{ 'Params uid label' | trans }}"
default: ''
help_text: "{{ 'Params uid help text' | trans }}"
i18n:
AIX account change secret:
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
@@ -60,6 +66,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params uid help text:
zh: '请输入用户ID'
ja: 'ユーザーIDを入力してください'
en: 'Please enter the user ID'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
@@ -75,3 +86,7 @@ i18n:
ja: 'グループ'
en: 'Groups'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'
en: 'User ID'

View File

@@ -14,27 +14,15 @@
- name: "Add {{ account.username }} user"
ansible.builtin.user:
name: "{{ account.username }}"
shell: "{{ params.shell }}"
home: "{{ params.home | default('/home/' + account.username, true) }}"
groups: "{{ params.groups }}"
uid: "{{ params.uid | int if params.uid | 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 }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1
state: present
when: user_info.failed
- name: "Add {{ account.username }} group"
ansible.builtin.group:
name: "{{ account.username }}"
state: present
when: user_info.failed
- name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when:
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
@@ -54,14 +42,40 @@
ignore_errors: true
when: account.secret_type == "password"
- name: remove jumpserver ssh key
- name: "Get home directory for {{ account.username }}"
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile:
dest: "{{ ssh_params.dest }}"
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
regexp: "{{ ssh_params.regexp }}"
state: absent
when:
- account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key:
@@ -79,7 +93,7 @@
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
become: "{{ account.become.ansible_become | default(False) }}"
become_method: su
become_user: "{{ account.become.ansible_user | default('') }}"
@@ -95,7 +109,7 @@
login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost

View File

@@ -36,6 +36,12 @@ params:
default: ''
help_text: "{{ 'Params groups help text' | trans }}"
- name: uid
type: str
label: "{{ 'Params uid label' | trans }}"
default: ''
help_text: "{{ 'Params uid help text' | trans }}"
i18n:
Posix account change secret:
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
@@ -62,6 +68,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params uid help text:
zh: '请输入用户ID'
ja: 'ユーザーIDを入力してください'
en: 'Please enter the user ID'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
@@ -77,3 +88,7 @@ i18n:
ja: 'グループ'
en: 'Groups'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'
en: 'User ID'

View File

@@ -25,11 +25,11 @@
- name: Verify password (pyfreerdp)
rdp_ping:
login_host: "{{ jms_asset.address }}"
login_host: "{{ jms_asset.origin_address }}"
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_gateway | default(None) }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -50,9 +50,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
username = account.username
path = f'/{username}' if username == "root" else f'/home/{username}'
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
return kwargs
@@ -130,6 +127,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
recorder = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret,
comment=f'{account.username}@{asset.address}'
)
records.append(recorder)
else:

View File

@@ -3,6 +3,9 @@
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Get info
@@ -12,9 +15,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: users
register: db_info

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Get info
@@ -11,6 +15,10 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
filter: "roles"
register: db_info

View File

@@ -95,12 +95,14 @@ class GatherAccountsManager(AccountBasePlaybookManager):
return None, None
users = User.objects.filter(id__in=recipients)
if not users:
if not users.exists():
return users, None
asset_ids = self.asset_username_mapper.keys()
assets = Asset.objects.filter(id__in=asset_ids)
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
asset_id_map = {str(asset.id): asset for asset in assets}
asset_id_username = list(assets.values_list('id', 'accounts__username'))
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
@@ -109,26 +111,24 @@ class GatherAccountsManager(AccountBasePlaybookManager):
for asset_id, username in asset_id_username:
system_asset_username_mapper[str(asset_id)].add(username)
change_info = {}
change_info = defaultdict(dict)
for asset_id, usernames in self.asset_username_mapper.items():
system_usernames = system_asset_username_mapper.get(asset_id)
if not system_usernames:
continue
add_usernames = usernames - system_usernames
remove_usernames = system_usernames - usernames
k = f'{asset_id_map[asset_id]}[{asset_id}]'
if not add_usernames and not remove_usernames:
continue
change_info[k] = {
'add_usernames': ', '.join(add_usernames),
'remove_usernames': ', '.join(remove_usernames),
change_info[str(asset_id_map[asset_id])] = {
'add_usernames': add_usernames,
'remove_usernames': remove_usernames
}
return users, change_info
return users, dict(change_info)
@staticmethod
def send_email_if_need(users, change_info):

View File

@@ -4,6 +4,9 @@
ansible_python_interpreter: /opt/py3/bin/python
db_name: "{{ jms_asset.spec_info.db_name }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Test MySQL connection
@@ -13,9 +16,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version
register: db_info
@@ -30,9 +33,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
@@ -47,7 +50,7 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Test PostgreSQL connection
@@ -11,6 +15,10 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
register: result
failed_when: not result.is_available
@@ -28,6 +36,10 @@
db: "{{ jms_asset.spec_info.db_name }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
role_attr_flags: LOGIN
ignore_errors: true
when: result is succeeded
@@ -40,6 +52,10 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
when:
- result is succeeded
- change_info is succeeded

View File

@@ -14,27 +14,15 @@
- name: "Add {{ account.username }} user"
ansible.builtin.user:
name: "{{ account.username }}"
shell: "{{ params.shell }}"
home: "{{ params.home | default('/home/' + account.username, true) }}"
groups: "{{ params.groups }}"
uid: "{{ params.uid | int if params.uid | 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 }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1
state: present
when: user_info.failed
- name: "Add {{ account.username }} group"
ansible.builtin.group:
name: "{{ account.username }}"
state: present
when: user_info.failed
- name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when:
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
@@ -54,14 +42,40 @@
ignore_errors: true
when: account.secret_type == "password"
- name: remove jumpserver ssh key
- name: "Get home directory for {{ account.username }}"
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile:
dest: "{{ ssh_params.dest }}"
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
regexp: "{{ ssh_params.regexp }}"
state: absent
when:
- account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key:
@@ -79,7 +93,7 @@
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
become: "{{ account.become.ansible_become | default(False) }}"
become_method: su
become_user: "{{ account.become.ansible_user | default('') }}"
@@ -95,7 +109,7 @@
login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost

View File

@@ -34,6 +34,12 @@ params:
default: ''
help_text: "{{ 'Params groups help text' | trans }}"
- name: uid
type: str
label: "{{ 'Params uid label' | trans }}"
default: ''
help_text: "{{ 'Params uid help text' | trans }}"
i18n:
Aix account push:
zh: '使用 Ansible 模块 user 执行 Aix 账号推送 (DES)'
@@ -60,6 +66,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params uid help text:
zh: '请输入用户ID'
ja: 'ユーザーIDを入力してください'
en: 'Please enter the user ID'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
@@ -75,3 +86,7 @@ i18n:
ja: 'グループ'
en: 'Groups'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'
en: 'User ID'

View File

@@ -14,27 +14,15 @@
- name: "Add {{ account.username }} user"
ansible.builtin.user:
name: "{{ account.username }}"
shell: "{{ params.shell }}"
home: "{{ params.home | default('/home/' + account.username, true) }}"
groups: "{{ params.groups }}"
uid: "{{ params.uid | int if params.uid | 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 }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1
state: present
when: user_info.failed
- name: "Add {{ account.username }} group"
ansible.builtin.group:
name: "{{ account.username }}"
state: present
when: user_info.failed
- name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when:
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
@@ -54,14 +42,40 @@
ignore_errors: true
when: account.secret_type == "password"
- name: remove jumpserver ssh key
- name: "Get home directory for {{ account.username }}"
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile:
dest: "{{ ssh_params.dest }}"
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
regexp: "{{ ssh_params.regexp }}"
state: absent
when:
- account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key:
@@ -79,7 +93,7 @@
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
become: "{{ account.become.ansible_become | default(False) }}"
become_method: su
become_user: "{{ account.become.ansible_user | default('') }}"
@@ -95,7 +109,7 @@
login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost

View File

@@ -36,6 +36,12 @@ params:
default: ''
help_text: "{{ 'Params groups help text' | trans }}"
- name: uid
type: str
label: "{{ 'Params uid label' | trans }}"
default: ''
help_text: "{{ 'Params uid help text' | trans }}"
i18n:
Posix account push:
zh: '使用 Ansible 模块 user 执行账号推送 (sha512)'
@@ -62,6 +68,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params uid help text:
zh: '请输入用户ID'
ja: 'ユーザーIDを入力してください'
en: 'Please enter the user ID'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
@@ -75,4 +86,9 @@ i18n:
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'
en: 'Groups'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'
en: 'User ID'

View File

@@ -25,11 +25,11 @@
- name: Verify password (pyfreerdp)
rdp_ping:
login_host: "{{ jms_asset.address }}"
login_host: "{{ jms_asset.origin_address }}"
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_gateway | default(None) }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -3,6 +3,9 @@
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: "Remove account"
@@ -12,8 +15,8 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
name: "{{ account.username }}"
state: absent

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: "Remove account"
@@ -12,4 +16,8 @@
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
name: "{{ account.username }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
state: absent

View File

@@ -13,4 +13,3 @@
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"

View File

@@ -3,6 +3,9 @@
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Verify account
@@ -12,7 +15,7 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Verify account
@@ -11,5 +15,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
register: result
failed_when: not result.is_available

View File

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

View File

@@ -0,0 +1,30 @@
# Generated by Django 4.1.13 on 2024-08-26 09:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_myasset'),
('accounts', '0003_automation'),
]
operations = [
migrations.AlterField(
model_name='changesecretrecord',
name='account',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.account'),
),
migrations.AlterField(
model_name='changesecretrecord',
name='asset',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset'),
),
migrations.AlterField(
model_name='changesecretrecord',
name='execution',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.automationexecution'),
),
]

View File

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

View File

@@ -33,16 +33,15 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
class ChangeSecretRecord(JMSBaseModel):
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.CASCADE)
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.SET_NULL, null=True)
asset = models.ForeignKey('assets.Asset', on_delete=models.SET_NULL, null=True)
account = models.ForeignKey('accounts.Account', on_delete=models.SET_NULL, null=True)
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
status = models.CharField(
max_length=16, verbose_name=_('Status'),
default=ChangeSecretRecordStatusChoice.pending.value
max_length=16, verbose_name=_('Status'), default=ChangeSecretRecordStatusChoice.pending.value
)
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
@@ -51,4 +50,4 @@ class ChangeSecretRecord(JMSBaseModel):
verbose_name = _("Change secret record")
def __str__(self):
return self.account.__str__()
return f'{self.account.username}@{self.asset}'

View File

@@ -178,7 +178,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
instance.save()
return instance, 'updated'
else:
raise serializers.ValidationError('Account already exists')
raise serializers.ValidationError(_('Account already exists'))
def create(self, validated_data):
push_now = validated_data.pop('push_now', None)
@@ -247,6 +247,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'name': {'required': False},
'source_id': {'required': False, 'allow_null': True},
}
fields_unimport_template = ['params']
@classmethod
def setup_eager_loading(cls, queryset):

View File

@@ -10,7 +10,7 @@ from .base import BaseAccountSerializer
class PasswordRulesSerializer(serializers.Serializer):
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
length = serializers.IntegerField(min_value=8, max_value=36, default=16, label=_('Password length'))
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
digit = serializers.BooleanField(default=True, label=_('Digit'))
@@ -19,6 +19,16 @@ class PasswordRulesSerializer(serializers.Serializer):
default='', allow_blank=True, max_length=16, label=_('Exclude symbol')
)
@staticmethod
def get_render_help_text():
return _("""length is the length of the password, and the range is 8 to 30.
lowercase indicates whether the password contains lowercase letters,
uppercase indicates whether it contains uppercase letters,
digit indicates whether it contains numbers, and symbol indicates whether it contains special symbols.
exclude_symbols is used to exclude specific symbols. You can fill in the symbol characters to be excluded (up to 16).
If you do not need to exclude symbols, you can leave it blank.
default: {"length": 16, "lowercase": true, "uppercase": true, "digit": true, "symbol": true, "exclude_symbols": ""}""")
class AccountTemplateSerializer(BaseAccountSerializer):
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
@@ -46,6 +56,7 @@ class AccountTemplateSerializer(BaseAccountSerializer):
'required': False
},
}
fields_unimport_template = ['push_params']
@staticmethod
def generate_secret(attrs):

View File

@@ -75,19 +75,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
if self.initial_data.get('secret_strategy') == SecretStrategy.custom:
return password_rules
length = password_rules.get('length')
try:
length = int(length)
except Exception as e:
logger.error(e)
msg = _("* Please enter the correct password length")
raise serializers.ValidationError(msg)
if length < 6 or length > 30:
msg = _('* Password length range 6-30 bits')
raise serializers.ValidationError(msg)
return password_rules
def validate(self, attrs):

View File

@@ -1,9 +1,15 @@
import datetime
from celery import shared_task
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _, gettext_noop
from accounts.const import AutomationTypes
from accounts.tasks.common import quickstart_automation_by_snapshot
from common.utils import get_logger, get_object_or_none
from common.const.crontab import CRONTAB_AT_AM_THREE
from common.utils import get_logger, get_object_or_none, get_log_keep_day
from ops.celery.decorator import register_as_period_task
from orgs.utils import tmp_to_org, tmp_to_root_org
logger = get_logger(__file__)
@@ -22,8 +28,14 @@ def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
@shared_task(
queue='ansible', verbose_name=_('Account execute automation'),
activity_callback=task_activity_callback
queue='ansible',
verbose_name=_('Account execute automation'),
activity_callback=task_activity_callback,
description=_(
"""Unified execution entry for account automation tasks: when the system performs tasks
such as account push, password change, account verification, account collection,
and gateway account verification, all tasks are executed through this unified entry"""
)
)
def execute_account_automation_task(pid, trigger, tp):
model = AutomationTypes.get_type_model(tp)
@@ -48,8 +60,12 @@ def record_task_activity_callback(self, record_ids, *args, **kwargs):
@shared_task(
queue='ansible', verbose_name=_('Execute automation record'),
activity_callback=record_task_activity_callback
queue='ansible',
verbose_name=_('Execute automation record'),
activity_callback=record_task_activity_callback,
description=_(
"""When manually executing password change records, this task is used"""
)
)
def execute_automation_record_task(record_ids, tp):
from accounts.models import ChangeSecretRecord
@@ -74,3 +90,33 @@ def execute_automation_record_task(record_ids, tp):
}
with tmp_to_org(record.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
@shared_task(
verbose_name=_('Clean change secret and push record period'),
description=_(
"""The system will periodically clean up unnecessary password change and push records,
including their associated change tasks, execution logs, assets, and accounts. When any
of these associated items are deleted, the corresponding password change and push records
become invalid. Therefore, to maintain a clean and efficient database, the system will
clean up expired records at 2 a.m daily, based on the interval specified by
PERM_EXPIRED_CHECK_PERIODIC in the config.txt configuration file. This periodic cleanup
mechanism helps free up storage space and enhances the security and overall performance
of data management"""
)
)
@register_as_period_task(crontab=CRONTAB_AT_AM_THREE)
def clean_change_secret_and_push_record_period():
from accounts.models import ChangeSecretRecord
print('Start clean change secret and push record period')
with tmp_to_root_org():
now = timezone.now()
days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days)
records = ChangeSecretRecord.objects.filter(
date_updated__lt=expired_day
).filter(
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
)
records.delete()

View File

@@ -22,7 +22,13 @@ def task_activity_callback(self, pid, trigger, *args, **kwargs):
return resource_ids, org_id
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
@shared_task(
verbose_name=_('Execute account backup plan'),
activity_callback=task_activity_callback,
description=_(
"When performing scheduled or manual account backups, this task is used"
)
)
def execute_account_backup_task(pid, trigger, **kwargs):
from accounts.models import AccountBackupAutomation
with tmp_to_root_org():

View File

@@ -26,8 +26,10 @@ def gather_asset_accounts_util(nodes, task_name):
@shared_task(
queue="ansible", verbose_name=_('Gather asset accounts'),
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None)
queue="ansible",
verbose_name=_('Gather asset accounts'),
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
description=_("Unused")
)
def gather_asset_accounts_task(node_ids, task_name=None):
if task_name is None:

View File

@@ -12,8 +12,12 @@ __all__ = [
@shared_task(
queue="ansible", verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
queue="ansible",
verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_(
"When creating or modifying an account requires account push, this task is executed"
)
)
def push_accounts_to_assets_task(account_ids, params=None):
from accounts.models import PushAccountAutomation

View File

@@ -21,8 +21,13 @@ __all__ = ['remove_accounts_task']
@shared_task(
queue="ansible", verbose_name=_('Remove account'),
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None)
queue="ansible",
verbose_name=_('Remove account'),
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None),
description=_(
"""When clicking "Sync deletion" in 'Console - Gather Account - Gathered accounts' this
task will be executed"""
)
)
def remove_accounts_task(gather_account_ids):
from accounts.models import GatheredAccount
@@ -41,7 +46,15 @@ def remove_accounts_task(gather_account_ids):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
@shared_task(verbose_name=_('Clean historical accounts'))
@shared_task(
verbose_name=_('Clean historical accounts'),
description=_(
"""Each time an asset account is updated, a historical account is generated, so it is
necessary to clean up the asset account history. The system will clean up excess account
records at 2 a.m. daily based on the configuration in the "System settings - Features -
Account storage - Record limit"""
)
)
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
@tmp_to_root_org()
def clean_historical_accounts():

View File

@@ -9,7 +9,11 @@ from orgs.utils import tmp_to_root_org, tmp_to_org
@shared_task(
verbose_name=_('Template sync info to related accounts'),
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None)
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None),
description=_(
"""When clicking 'Sync new secret to accounts' in 'Console - Account - Templates -
Accounts' this task will be executed"""
)
)
def template_sync_related_accounts(template_id, user_id=None):
from accounts.models import Account, AccountTemplate

View File

@@ -28,7 +28,12 @@ def sync_instance(instance):
return "succeeded", msg
@shared_task(verbose_name=_('Sync secret to vault'))
@shared_task(
verbose_name=_('Sync secret to vault'),
description=_(
"When clicking 'Sync' in 'System Settings - Features - Account Storage' this task will be executed"
)
)
def sync_secret_to_vault():
if not vault_client.enabled:
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型

View File

@@ -4,7 +4,6 @@ from django.utils.translation import gettext_noop
from accounts.const import AutomationTypes
from accounts.tasks.common import quickstart_automation_by_snapshot
from assets.const import GATEWAY_NAME
from common.utils import get_logger
from orgs.utils import org_aware_func
@@ -32,13 +31,13 @@ def verify_accounts_connectivity_util(accounts, task_name):
asset_ids = [a.asset_id for a in accounts]
assets = Asset.objects.filter(id__in=asset_ids)
gateways = assets.filter(platform__name=GATEWAY_NAME)
gateways = assets.gateways()
verify_connectivity_util(
gateways, AutomationTypes.verify_gateway_account,
accounts, task_name
)
common_assets = assets.exclude(platform__name=GATEWAY_NAME)
common_assets = assets.gateways(0)
verify_connectivity_util(
common_assets, AutomationTypes.verify_account,
accounts, task_name
@@ -46,8 +45,12 @@ def verify_accounts_connectivity_util(accounts, task_name):
@shared_task(
queue="ansible", verbose_name=_('Verify asset account availability'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
queue="ansible",
verbose_name=_('Verify asset account availability'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_(
"When clicking 'Test' in 'Console - Asset details - Accounts' this task will be executed"
)
)
def verify_accounts_connectivity_task(account_ids):
from accounts.models import Account, VerifyAccountAutomation

View File

@@ -1,18 +1,29 @@
{% load i18n %}
<h3>{% trans 'Gather account change information' %}</h3>
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
<h3></h3>
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; text-align: left; margin-top: 20px;">
<caption></caption>
<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 'Added account' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
{% trans 'Asset' %}
</th>
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
{% trans 'Added account' %}
</th>
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
{% trans 'Deleted account' %}
</th>
</tr>
{% for name, change in change_info.items %}
<tr style="{% cycle 'background-color: #ebf5ff;' 'background-color: #fff;' %}">
<td style="border: 1px solid #ddd; padding: 10px;">{{ name }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.add_usernames }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.remove_usernames }}</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
{{ name | safe }}
</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
{{ change.add_usernames | join:" " | safe }}
</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
{{ change.remove_usernames | join:" " | safe }}
</td>
</tr>
{% endfor %}
</table>

View File

@@ -8,3 +8,4 @@ class ActionChoices(models.TextChoices):
review = 'review', _('Review')
warning = 'warning', _('Warn')
notice = 'notice', _('Notify')
notify_and_warn = 'notify_and_warn', _('Notify and warn')

View File

@@ -62,7 +62,7 @@ class ActionAclSerializer(serializers.Serializer):
self.set_action_choices()
class Meta:
action_choices_exclude = [ActionChoices.warning]
action_choices_exclude = [ActionChoices.warning, ActionChoices.notify_and_warn]
def set_action_choices(self):
field_action = self.fields.get("action")

View File

@@ -2,10 +2,10 @@
#
from collections import defaultdict
import django_filters
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from django_filters import rest_framework as drf_filters
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
@@ -22,6 +22,7 @@ from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend
from common.utils import get_logger, is_uuid
from orgs.mixins import generics
from orgs.mixins.api import OrgBulkModelViewSet
from ...const import GATEWAY_NAME
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
logger = get_logger(__file__)
@@ -32,31 +33,32 @@ __all__ = [
class AssetFilterSet(BaseFilterSet):
platform = django_filters.CharFilter(method='filter_platform')
exclude_platform = django_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
domain = django_filters.CharFilter(method='filter_domain')
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
protocols = django_filters.CharFilter(method='filter_protocols')
domain_enabled = django_filters.BooleanFilter(
platform = drf_filters.CharFilter(method='filter_platform')
is_gateway = drf_filters.BooleanFilter(method='filter_is_gateway')
exclude_platform = drf_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
domain = drf_filters.CharFilter(method='filter_domain')
type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
protocols = drf_filters.CharFilter(method='filter_protocols')
domain_enabled = drf_filters.BooleanFilter(
field_name="platform__domain_enabled", lookup_expr="exact"
)
ping_enabled = django_filters.BooleanFilter(
ping_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__ping_enabled", lookup_expr="exact"
)
gather_facts_enabled = django_filters.BooleanFilter(
gather_facts_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__gather_facts_enabled", lookup_expr="exact"
)
change_secret_enabled = django_filters.BooleanFilter(
change_secret_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__change_secret_enabled", lookup_expr="exact"
)
push_account_enabled = django_filters.BooleanFilter(
push_account_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__push_account_enabled", lookup_expr="exact"
)
verify_account_enabled = django_filters.BooleanFilter(
verify_account_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__verify_account_enabled", lookup_expr="exact"
)
gather_accounts_enabled = django_filters.BooleanFilter(
gather_accounts_enabled = drf_filters.BooleanFilter(
field_name="platform__automation__gather_accounts_enabled", lookup_expr="exact"
)
@@ -71,9 +73,16 @@ class AssetFilterSet(BaseFilterSet):
def filter_platform(queryset, name, value):
if value.isdigit():
return queryset.filter(platform_id=value)
elif value == GATEWAY_NAME:
return queryset.filter(platform__name__istartswith=GATEWAY_NAME)
else:
return queryset.filter(platform__name=value)
@staticmethod
def filter_is_gateway(queryset, name, value):
queryset = queryset.gateways(value)
return queryset
@staticmethod
def filter_domain(queryset, name, value):
if is_uuid(value):
@@ -298,6 +307,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
def check_permissions(self, request):
action_perm_require = {
"refresh": "assets.refresh_assethardwareinfo",
"test": "assets.test_assetconnectivity",
}
_action = request.data.get("action")
perm_required = action_perm_require.get(_action)

View File

@@ -1,4 +1,5 @@
from django.db.models import Count
from django_filters import rest_framework as filters
from rest_framework import generics
from rest_framework import serializers
from rest_framework.decorators import action
@@ -14,6 +15,14 @@ from common.serializers import GroupedChoiceSerializer
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet']
class PlatformFilter(filters.FilterSet):
name__startswith = filters.CharFilter(field_name='name', lookup_expr='istartswith')
class Meta:
model = Platform
fields = ['name', 'category', 'type']
class AssetPlatformViewSet(JMSModelViewSet):
queryset = Platform.objects.all()
serializer_classes = {
@@ -21,7 +30,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
'list': PlatformListSerializer,
'categories': GroupedChoiceSerializer,
}
filterset_fields = ['name', 'category', 'type']
filterset_class = PlatformFilter
search_fields = ['name']
ordering = ['-internal', 'name']
rbac_perms = {

View File

@@ -170,6 +170,7 @@ class BasePlaybookManager:
result = self.write_cert_to_file(
os.path.join(cert_dir, f), specific.get(f)
)
os.chmod(result, 0o600)
host['jms_asset']['secret_info'][f] = result
return host

View File

@@ -3,6 +3,9 @@
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Get info
@@ -12,9 +15,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version
register: db_info

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Get info
@@ -11,6 +15,10 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
register: db_info
- name: Define info by set_fact

View File

@@ -13,4 +13,3 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"

View File

@@ -3,6 +3,9 @@
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Test MySQL connection
@@ -12,7 +15,7 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version

View File

@@ -2,6 +2,10 @@
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks:
- name: Test PostgreSQL connection
@@ -11,5 +15,9 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | 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 }}"
register: result
failed_when: not result.is_available

View File

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

View File

@@ -1,6 +1,7 @@
from .automation import *
from .base import *
from .category import *
from .database import *
from .host import *
from .platform import *
from .protocol import *

View File

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

View File

@@ -1,3 +1,5 @@
from django.db.models import TextChoices
from .base import BaseType
@@ -120,3 +122,10 @@ class DatabaseTypes(BaseType):
cls.MYSQL, cls.MARIADB, cls.POSTGRESQL,
cls.MONGODB, cls.REDIS,
]
class PostgresqlSSLMode(TextChoices):
PREFER = 'prefer', 'Prefer'
REQUIRE = 'require', 'Require'
VERIFY_CA = 'verify-ca', 'Verify CA'
VERIFY_FULL = 'verify-full', 'Verify Full'

View File

@@ -45,6 +45,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': False,
'label': _('Old SSH version'),
'help_text': _('Old SSH version like openssh 5.x or 6.x')
},
'nc': {
'type': 'bool',
'default': False,
'label': 'Netcat (nc)',
'help_text': _('Netcat help text')
}
}
},

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.1.13 on 2024-09-13 08:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_myasset'),
]
operations = [
migrations.AddField(
model_name='database',
name='pg_ssl_mode',
field=models.CharField(choices=[
('prefer', 'Prefer'),
('require', 'Require'),
('verify-ca', 'Verify CA'),
('verify-full', 'Verify Full')
], default='prefer',
max_length=16, verbose_name='Postgresql SSL mode'),
),
]

View File

@@ -38,6 +38,13 @@ class AssetQuerySet(models.QuerySet):
def valid(self):
return self.active()
def gateways(self, is_gateway=1):
kwargs = {'platform__name__startswith': 'Gateway'}
if is_gateway:
return self.filter(**kwargs)
else:
return self.exclude(**kwargs)
def has_protocol(self, name):
return self.filter(protocols__contains=name)
@@ -158,10 +165,16 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
name = models.CharField(max_length=128, verbose_name=_('Name'))
address = models.CharField(max_length=767, verbose_name=_('Address'), db_index=True)
platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets',
verbose_name=_("Zone"), on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
platform = models.ForeignKey(
Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets'
)
domain = models.ForeignKey(
"assets.Domain", null=True, blank=True, related_name='assets',
verbose_name=_("Zone"), on_delete=models.SET_NULL
)
nodes = models.ManyToManyField(
'assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")
)
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)

View File

@@ -1,6 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from assets.const import PostgresqlSSLMode
from common.db.fields import EncryptTextField
from .common import Asset
@@ -12,6 +13,10 @@ class Database(Asset):
client_cert = EncryptTextField(verbose_name=_("Client cert"), blank=True)
client_key = EncryptTextField(verbose_name=_("Client key"), blank=True)
allow_invalid_cert = models.BooleanField(default=False, verbose_name=_('Allow invalid cert'))
pg_ssl_mode = models.CharField(
max_length=16, choices=PostgresqlSSLMode.choices,
default=PostgresqlSSLMode.PREFER, verbose_name=_('Postgresql SSL mode')
)
def __str__(self):
return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name)

View File

@@ -31,7 +31,7 @@ class Domain(LabeledMixin, JMSOrgBaseModel):
@lazyproperty
def assets_amount(self):
return self.assets.exclude(platform__name='Gateway').count()
return self.assets.gateways(0).count()
def random_gateway(self):
gateways = [gw for gw in self.active_gateways if gw.is_connective]

View File

@@ -16,7 +16,7 @@ __all__ = ['Gateway']
class GatewayManager(OrgManager):
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(platform__name=GATEWAY_NAME)
queryset = queryset.filter(platform__name__startswith=GATEWAY_NAME)
return queryset
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
@@ -33,10 +33,6 @@ class Gateway(Host):
proxy = True
verbose_name = _("Gateway")
def save(self, *args, **kwargs):
self.platform = self.default_platform()
return super().save(*args, **kwargs)
@classmethod
def default_platform(cls):
return Platform.objects.get(name=GATEWAY_NAME, internal=True)

View File

@@ -18,7 +18,7 @@ from common.serializers.fields import LabeledChoiceField
from labels.models import Label
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes
from ...models import Asset, Node, Platform, Protocol
from ...models import Asset, Node, Platform, Protocol, Host, Device, Database, Cloud, Web, Custom
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
@@ -31,6 +31,12 @@ __all__ = [
class AssetProtocolsSerializer(serializers.ModelSerializer):
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=0)
def get_render_help_text(self):
if self.parent and self.parent.many:
return _('Protocols, format is ["protocol/port"]')
else:
return _('Protocol, format is name/port')
def to_file_representation(self, data):
return '{name}/{port}'.format(**data)
@@ -97,6 +103,9 @@ class AssetAccountSerializer(AccountSerializer):
attrs = super().validate(attrs)
return self.set_secret(attrs)
def get_render_help_text(self):
return _('Accounts, format [{"name": "x", "username": "x", "secret": "x", "secret_type": "password"}]')
class Meta(AccountSerializer.Meta):
fields = [
f for f in AccountSerializer.Meta.fields
@@ -121,19 +130,30 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
}
class NodeDisplaySerializer(serializers.ListField):
def get_render_help_text(self):
return _('Node path, format ["/org_name/node_name"], if node not exist, will create it')
def to_internal_value(self, data):
return data
def to_representation(self, data):
return data
class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
_accounts = None
class Meta:
model = Asset
fields_mini = ['id', 'name', 'address']
fields_small = fields_mini + ['is_active', 'comment']
fields_fk = ['domain', 'platform']
fields_mini = ['id', 'name', 'address'] + fields_fk
fields_small = fields_mini + ['is_active', 'comment']
fields_m2m = [
'nodes', 'labels', 'protocols',
'nodes_display', 'accounts',
@@ -289,6 +309,17 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
})
return protocols_data_map.values()
def validate_platform(self, platform_data):
check_models = {Host, Device, Database, Cloud, Web, Custom}
if self.Meta.model not in check_models:
return platform_data
model_name = self.Meta.model.__name__.lower()
if model_name != platform_data.category:
raise serializers.ValidationError({
'platform': f"Platform does not match: {platform_data.name}"
})
return platform_data
@staticmethod
def update_account_su_from(accounts, include_su_from_accounts):
if not include_su_from_accounts:

View File

@@ -16,6 +16,7 @@ class CustomSerializer(AssetSerializer):
class Meta(AssetSerializer.Meta):
model = Custom
fields = AssetSerializer.Meta.fields + ['custom_info']
fields_unimport_template = ['custom_info']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -16,9 +16,14 @@ class DatabaseSerializer(AssetSerializer):
model = Database
extra_fields = [
'db_name', 'use_ssl', 'ca_cert', 'client_cert',
'client_key', 'allow_invalid_cert'
'client_key', 'allow_invalid_cert', 'pg_ssl_mode'
]
fields = AssetSerializer.Meta.fields + extra_fields
extra_kwargs = {
'ca_cert': {'help_text': _('CA cert help text')},
'pg_ssl_mode': {'help_text': _('Postgresql ssl model help text')},
}
extra_kwargs.update(AssetSerializer.Meta.extra_kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -68,7 +68,7 @@ class DomainListSerializer(DomainSerializer):
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.annotate(
assets_amount=Count('assets', filter=~Q(assets__platform__name='Gateway'), distinct=True),
assets_amount=Count('assets', filter=~Q(assets__platform__name__startswith='Gateway'), distinct=True),
)
return queryset

View File

@@ -14,6 +14,11 @@ class GatewaySerializer(HostSerializer):
class Meta(HostSerializer.Meta):
model = Gateway
def validate_platform(self, p):
if not p.name.startswith('Gateway'):
raise serializers.ValidationError(_('The platform must start with Gateway'))
return p
def validate_name(self, value):
queryset = Asset.objects.filter(name=value)
if self.instance:

View File

@@ -147,6 +147,10 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
name, port = data.split('/')
return {'name': name, 'port': port}
@staticmethod
def get_render_help_text():
return _('Protocols, format is ["protocol/port"]')
class PlatformCustomField(serializers.Serializer):
TYPE_CHOICES = [(t, t) for t, c in type_field_map.items()]

View File

@@ -21,8 +21,10 @@ def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
@shared_task(
queue='ansible', verbose_name=_('Asset execute automation'),
activity_callback=task_activity_callback
queue='ansible',
verbose_name=_('Asset execute automation'),
activity_callback=task_activity_callback,
description=_("Unused")
)
def execute_asset_automation_task(pid, trigger, tp):
model = AutomationTypes.get_type_model(tp)

View File

@@ -18,8 +18,13 @@ __all__ = [
@shared_task(
queue="ansible", verbose_name=_('Gather assets facts'),
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
queue="ansible",
verbose_name=_('Gather assets facts'),
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
description=_(
"""When clicking 'Refresh hardware info' in 'Console - Asset Details - Basic' this task
will be executed"""
)
)
def gather_assets_facts_task(asset_ids, org_id, task_name=None):
from assets.models import GatherFactsAutomation

View File

@@ -1,19 +1,25 @@
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from assets.utils import check_node_assets_amount
from common.const.crontab import CRONTAB_AT_AM_TWO
from common.utils import get_logger
from common.utils.lock import AcquireFailed
from ops.celery.decorator import register_as_period_task
from orgs.models import Organization
from orgs.utils import tmp_to_org
from ops.celery.decorator import register_as_period_task
from assets.utils import check_node_assets_amount
from common.utils.lock import AcquireFailed
from common.utils import get_logger
from common.const.crontab import CRONTAB_AT_AM_TWO
logger = get_logger(__file__)
@shared_task(verbose_name=_('Check the amount of assets under the node'))
@shared_task(
verbose_name=_('Check the amount of assets under the node'),
description=_(
"""Manually verifying asset quantities updates the asset count for nodes under the
current organization. This task will be called in the following two cases: when updating
nodes and when the number of nodes exceeds 100"""
)
)
def check_node_assets_amount_task(org_id=None):
if org_id is None:
orgs = Organization.objects.all()
@@ -30,7 +36,13 @@ def check_node_assets_amount_task(org_id=None):
logger.error(error)
@shared_task(verbose_name=_('Periodic check the amount of assets under the node'))
@shared_task(
verbose_name=_('Periodic check the amount of assets under the node'),
description=_(
"""Schedule the check_node_assets_amount_task to periodically update the asset count of
all nodes under all organizations"""
)
)
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def check_node_assets_amount_period_task():
check_node_assets_amount_task()

View File

@@ -17,8 +17,12 @@ __all__ = [
@shared_task(
verbose_name=_('Test assets connectivity'), queue='ansible',
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
verbose_name=_('Test assets connectivity'),
queue='ansible',
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
description=_(
"When clicking 'Test Asset Connectivity' in 'Asset Details - Basic Settings' this task will be executed"
)
)
def test_assets_connectivity_task(asset_ids, org_id, task_name=None):
from assets.models import PingAutomation

View File

@@ -16,8 +16,12 @@ __all__ = [
@shared_task(
verbose_name=_('Test gateways connectivity'), queue='ansible',
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
verbose_name=_('Test gateways connectivity'),
queue='ansible',
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
description=_(
"When clicking 'Test Connection' in 'Domain Details - Gateway' this task will be executed"
)
)
def test_gateways_connectivity_task(asset_ids, org_id, local_port, task_name=None):
from assets.models import PingAutomation
@@ -33,4 +37,5 @@ def test_gateways_connectivity_task(asset_ids, org_id, local_port, task_name=Non
def test_gateways_connectivity_manual(gateway_ids, local_port):
task_name = gettext_noop("Test gateways connectivity")
gateway_ids = [str(i) for i in gateway_ids]
return test_gateways_connectivity_task.delay(gateway_ids, str(current_org.id), local_port, task_name)
return test_gateways_connectivity_task.delay(gateway_ids, str(current_org.id), local_port,
task_name)

View File

@@ -1,2 +1 @@
from .k8s import *
from .node import *

View File

@@ -1,177 +0,0 @@
# -*- coding: utf-8 -*-
from urllib.parse import urlencode, urlparse
from kubernetes import client
from kubernetes.client import api_client
from kubernetes.client.api import core_v1_api
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
from common.utils import get_logger
from ..const import CloudTypes, Category
logger = get_logger(__file__)
class KubernetesClient:
def __init__(self, asset, token):
self.url = asset.address
self.token = token or ''
self.server = self.get_gateway_server(asset)
@property
def api(self):
configuration = client.Configuration()
scheme = urlparse(self.url).scheme
if not self.server:
host = self.url
else:
host = f'{scheme}://127.0.0.1:{self.server.local_bind_port}'
configuration.host = host
configuration.verify_ssl = False
configuration.api_key = {"authorization": "Bearer " + self.token}
c = api_client.ApiClient(configuration=configuration)
api = core_v1_api.CoreV1Api(c)
return api
def get_namespaces(self):
namespaces = []
resp = self.api.list_namespace()
for ns in resp.items:
namespaces.append(ns.metadata.name)
return namespaces
def get_pods(self, namespace):
pods = []
resp = self.api.list_namespaced_pod(namespace)
for pd in resp.items:
pods.append(pd.metadata.name)
return pods
def get_containers(self, namespace, pod_name):
containers = []
resp = self.api.read_namespaced_pod(pod_name, namespace)
for container in resp.spec.containers:
containers.append(container.name)
return containers
@staticmethod
def get_gateway_server(asset):
gateway = None
if not asset.is_gateway and asset.domain:
gateway = asset.domain.select_gateway()
if not gateway:
return
remote_bind_address = (
urlparse(asset.address).hostname,
urlparse(asset.address).port or 443
)
server = SSHTunnelForwarder(
(gateway.address, gateway.port),
ssh_username=gateway.username,
ssh_password=gateway.password,
ssh_pkey=gateway.private_key_path,
remote_bind_address=remote_bind_address
)
try:
server.start()
except BaseSSHTunnelForwarderError:
err_msg = 'Gateway is not active: %s' % asset.get('name', '')
print('\033[31m %s \033[0m\n' % err_msg)
return server
def run(self, tp, *args):
func_name = f'get_{tp}s'
data = []
if hasattr(self, func_name):
try:
data = getattr(self, func_name)(*args)
except Exception as e:
logger.error(f'K8S tree get {tp} error: {e}')
if self.server:
self.server.stop()
return data
class KubernetesTree:
def __init__(self, asset, secret):
self.asset = asset
self.secret = secret
def as_asset_tree_node(self):
i = str(self.asset.id)
name = str(self.asset)
node = self.create_tree_node(
i, i, name, 'asset', icon='k8s', is_open=True,
)
return node
def as_namespace_node(self, name, tp):
i = urlencode({'namespace': name})
pid = str(self.asset.id)
node = self.create_tree_node(i, pid, name, tp, icon='cloud')
return node
def as_pod_tree_node(self, namespace, name, tp):
pid = urlencode({'namespace': namespace})
i = urlencode({'namespace': namespace, 'pod': name})
node = self.create_tree_node(i, pid, name, tp, icon='cloud')
return node
def as_container_tree_node(self, namespace, pod, name, tp):
pid = urlencode({'namespace': namespace, 'pod': pod})
i = urlencode({'namespace': namespace, 'pod': pod, 'container': name})
node = self.create_tree_node(
i, pid, name, tp, icon='cloud', is_container=True
)
return node
@staticmethod
def create_tree_node(id_, pid, name, identity, icon='', is_container=False, is_open=False):
node = {
'id': id_,
'name': name,
'title': name,
'pId': pid,
'isParent': not is_container,
'open': is_open,
'iconSkin': icon,
'meta': {
'type': 'k8s',
'data': {
'category': Category.CLOUD,
'type': CloudTypes.K8S,
'identity': identity
}
}
}
return node
def async_tree_node(self, namespace, pod):
tree = []
k8s_client = KubernetesClient(self.asset, self.secret)
if pod:
tp = 'container'
containers = k8s_client.run(
tp, namespace, pod
)
for container in containers:
container_node = self.as_container_tree_node(
namespace, pod, container, tp
)
tree.append(container_node)
elif namespace:
tp = 'pod'
pods = k8s_client.run(tp, namespace)
for pod in pods:
pod_node = self.as_pod_tree_node(namespace, pod, tp)
tree.append(pod_node)
else:
tp = 'namespace'
namespaces = k8s_client.run(tp)
for namespace in namespaces:
namespace_node = self.as_namespace_node(namespace, tp)
tree.append(namespace_node)
return tree

View File

@@ -146,7 +146,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
username = self.request.user.username
q = Q(username=username) | Q(username__icontains=f'({username})')
qs = qs.filter(q)
return qs

View File

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

View File

@@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord'
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider',
}
include_models = {'UserSession'}
for i, app in enumerate(apps.get_models(), 1):

View File

@@ -128,7 +128,15 @@ def clean_expired_session_period():
logger.info("Clean session replay done")
@shared_task(verbose_name=_('Clean audits session task log'))
@shared_task(
verbose_name=_('Clean audits session task log'),
description=_(
"""Since the system generates login logs, operation logs, file upload logs, activity
logs, Celery execution logs, session recordings, command records, and password change
logs, it will perform cleanup of records that exceed the time limit according to the
'Tasks - Regular clean-up' in the system settings at 2 a.m daily"""
)
)
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clean_audits_log_period():
print("Start clean audit session task log")
@@ -142,7 +150,13 @@ def clean_audits_log_period():
clean_password_change_log_period()
@shared_task(verbose_name=_('Upload FTP file to external storage'))
@shared_task(
verbose_name=_('Upload FTP file to external storage'),
description=_(
"""If SERVER_REPLAY_STORAGE is configured, files uploaded through file management will be
synchronized to external storage"""
)
)
def upload_ftp_file_to_external_storage(ftp_log_id, file_name):
logger.info(f'Start upload FTP file record to external storage: {ftp_log_id} - {file_name}')
ftp_log = FTPLog.objects.filter(id=ftp_log_id).first()

View File

@@ -472,6 +472,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'check': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -484,6 +485,28 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
def get_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['GET'], detail=True, url_path='check')
def check(self, request, *args, **kwargs):
instance = self.get_object()
data = {
"detail": "OK",
"code": "perm_ok",
"expired": instance.is_expired
}
try:
self._validate_perm(
instance.user,
instance.asset,
instance.account,
instance.protocol
)
except JMSException as e:
data['code'] = e.detail.code
data['detail'] = str(e.detail)
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return Response(data=data, status=status.HTTP_200_OK)
@action(methods=['PATCH'], detail=False)
def renewal(self, request, *args, **kwargs):
from common.utils.timezone import as_current_tz

View File

@@ -1,6 +1,6 @@
# coding:utf-8
#
import abc
import ldap
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@@ -15,13 +15,16 @@ from .base import JMSBaseAuthBackend
logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
@staticmethod
def is_enabled():
return settings.AUTH_LDAP
class LDAPBaseBackend(LDAPBackend):
@abc.abstractmethod
def is_enabled(self):
raise NotImplementedError('is_enabled')
@property
@abc.abstractmethod
def is_user_login_only_in_users(self):
raise NotImplementedError('is_authenticated')
def get_or_build_user(self, username, ldap_user):
"""
@@ -56,38 +59,6 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
return user, built
def pre_check(self, username, password):
if not settings.AUTH_LDAP:
error = 'Not enabled auth ldap'
return False, error
if not username:
error = 'Username is None'
return False, error
if not password:
error = 'Password is None'
return False, error
if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS:
user_model = self.get_user_model()
exist = user_model.objects.filter(username=username).exists()
if not exist:
error = 'user ({}) is not in the user list'.format(username)
return False, error
return True, ''
def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password)
if not match:
logger.info('Authenticate failed: {}'.format(msg))
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user))
return user if self.user_can_authenticate(user) else None
def get_user(self, user_id):
user = None
try:
@@ -111,6 +82,67 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
user = ldap_user.populate_user()
return user
def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password)
if not match:
logger.info('Authenticate failed: {}'.format(msg))
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user))
return user if self.user_can_authenticate(user) else None
def pre_check(self, username, password):
if not self.is_enabled():
error = 'Not enabled auth ldap'
return False, error
if not username:
error = 'Username is None'
return False, error
if not password:
error = 'Password is None'
return False, error
if self.is_user_login_only_in_users:
user_model = self.get_user_model()
exist = user_model.objects.filter(username=username).exists()
if not exist:
error = 'user ({}) is not in the user list'.format(username)
return False, error
return True, ''
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
@staticmethod
def is_enabled():
return settings.AUTH_LDAP
@property
def is_user_login_only_in_users(self):
return settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
class LDAPHAAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
settings_prefix = "AUTH_LDAP_HA_"
@staticmethod
def is_enabled():
return settings.AUTH_LDAP_HA
@property
def is_user_login_only_in_users(self):
return settings.AUTH_LDAP_HA_USER_LOGIN_ONLY_IN_USERS
class LDAPUser(_LDAPUser):
@@ -126,13 +158,18 @@ class LDAPUser(_LDAPUser):
configuration in the settings.py file
is configured with a `lambda` problem value
"""
if isinstance(self.backend, LDAPAuthorizationBackend):
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_SEARCH_OU
else:
search_filter = settings.AUTH_LDAP_HA_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_HA_SEARCH_OU
user_search_union = [
LDAPSearch(
USER_SEARCH, ldap.SCOPE_SUBTREE,
settings.AUTH_LDAP_SEARCH_FILTER
search_filter
)
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|")
for USER_SEARCH in str(search_ou).split("|")
]
search = LDAPSearchUnion(*user_search_union)
@@ -169,7 +206,8 @@ class LDAPUser(_LDAPUser):
else:
value = is_true(value)
except LookupError:
logger.warning("{} does not have a value for the attribute {}".format(self.dn, attr))
logger.warning(
"{} does not have a value for the attribute {}".format(self.dn, attr))
else:
if not hasattr(self._user, field):
continue

View File

@@ -8,27 +8,26 @@
"""
import base64
import requests
from rest_framework.exceptions import ParseError
import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.urls import reverse
from django.conf import settings
from rest_framework.exceptions import ParseError
from common.utils import get_logger
from authentication.signals import user_auth_success, user_auth_failed
from authentication.utils import build_absolute_uri_for_oidc
from common.utils import get_logger
from users.utils import construct_user_email
from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user
)
from authentication.signals import user_auth_success, user_auth_failed
from .utils import validate_and_return_id_token
from ..base import JMSBaseAuthBackend
logger = get_logger(__file__)
@@ -55,16 +54,17 @@ class UserMixin:
logger.debug(log_prompt.format(user_attrs))
username = user_attrs.get('username')
name = user_attrs.get('name')
groups = user_attrs.pop('groups', None)
user, created = get_user_model().objects.get_or_create(
username=username, defaults=user_attrs
)
user_attrs['groups'] = groups
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => openid create or update user"))
openid_create_or_update_user.send(
sender=self.__class__, request=request, user=user, created=created,
name=name, username=username, email=email
sender=self.__class__, request=request, user=user,
created=created, attrs=user_attrs,
)
return user, created
@@ -269,7 +269,8 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
# Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint'))
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, timeout=request_timeout)
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload,
timeout=request_timeout)
try:
token_response.raise_for_status()
token_response_data = token_response.json()

View File

@@ -17,13 +17,16 @@ import time
from django.conf import settings
from django.contrib import auth
from django.core.exceptions import SuspiciousOperation
from django.db import IntegrityError
from django.http import HttpResponseRedirect, QueryDict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.http import urlencode
from django.views.generic import View
from django.utils.translation import gettext_lazy as _
from authentication.utils import build_absolute_uri_for_oidc
from authentication.views.mixins import FlashMessageMixin
from common.utils import safe_next_url
from .utils import get_logger
@@ -113,7 +116,7 @@ class OIDCAuthRequestView(View):
return HttpResponseRedirect(redirect_url)
class OIDCAuthCallbackView(View):
class OIDCAuthCallbackView(View, FlashMessageMixin):
""" Allows to complete the authentication process.
This view acts as the main endpoint to complete the authentication process involving the OIDC
@@ -165,7 +168,13 @@ class OIDCAuthCallbackView(View):
next_url = request.session.get('oidc_auth_next_url', None)
code_verifier = request.session.get('oidc_auth_code_verifier', None)
logger.debug(log_prompt.format('Process authenticate'))
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
try:
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
except IntegrityError:
title = _("OpenID Error")
msg = _('Please check if a user with the same username or email already exists')
response = self.get_failed_response('/', title, msg)
return response
if user:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)

View File

@@ -27,9 +27,13 @@ class SAML2Backend(JMSModelBackend):
log_prompt = "Get or Create user [SAML2Backend]: {}"
logger.debug(log_prompt.format('start'))
groups = saml_user_data.pop('groups', None)
user, created = get_user_model().objects.get_or_create(
username=saml_user_data['username'], defaults=saml_user_data
)
saml_user_data['groups'] = groups
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => saml2 create or update user"))

View File

@@ -3,8 +3,10 @@ from urllib import parse
from django.conf import settings
from django.contrib import auth
from django.db import IntegrityError
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from onelogin.saml2.auth import OneLogin_Saml2_Auth
@@ -14,6 +16,7 @@ from onelogin.saml2.idp_metadata_parser import (
dict_deep_merge
)
from authentication.views.mixins import FlashMessageMixin
from common.utils import get_logger
from .settings import JmsSaml2Settings
@@ -87,6 +90,7 @@ class PrepareRequestMixin:
('name', 'name', False),
('phone', 'phone', False),
('comment', 'comment', False),
('groups', 'groups', False),
)
attr_list = []
for name, friend_name, is_required in need_attrs:
@@ -185,7 +189,7 @@ class PrepareRequestMixin:
user_attrs = {}
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
valid_attrs = ['username', 'name', 'email', 'comment', 'phone', 'groups']
for attr, value in attrs.items():
attr = attr.rsplit('/', 1)[-1]
@@ -242,7 +246,7 @@ class Saml2EndSessionView(View, PrepareRequestMixin):
return HttpResponseRedirect(logout_url)
class Saml2AuthCallbackView(View, PrepareRequestMixin):
class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
def post(self, request):
log_prompt = "Process SAML2 POST requests: {}"
@@ -271,7 +275,13 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Process authenticate'))
saml_user_data = self.get_attributes(saml_instance)
user = auth.authenticate(request=request, saml_user_data=saml_user_data)
try:
user = auth.authenticate(request=request, saml_user_data=saml_user_data)
except IntegrityError:
title = _("SAML2 Error")
msg = _('Please check if a user with the same username or email already exists')
response = self.get_failed_response('/', title, msg)
return response
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)

View File

@@ -2,6 +2,7 @@ import base64
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.http import HttpResponse
from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
@@ -116,23 +117,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
@staticmethod
def set_cookie_public_key(request, response):
def set_cookie_public_key(self, request, response):
if request.path.startswith('/api'):
return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
session_public_key = request.session.get(session_public_key_name)
cookie_public_key = request.COOKIES.get(session_public_key_name)
if session_public_key and session_public_key == cookie_public_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
private_key, public_key = self.get_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
request.session[session_public_key_name] = public_key_decode
request.session[session_private_key_name] = private_key
response.set_cookie(session_public_key_name, public_key_decode)
def get_key_pair(self):
key_pair = cache.get(self.USER_LOGIN_ENCRYPTION_KEY_PAIR)
if key_pair:
return key_pair['private_key'], key_pair['public_key']
private_key, public_key = gen_key_pair()
key_pair = {
'private_key': private_key,
'public_key': public_key
}
cache.set(self.USER_LOGIN_ENCRYPTION_KEY_PAIR, key_pair, None)
return private_key, public_key
@staticmethod
def set_cookie_session_prefix(request, response):

View File

@@ -301,6 +301,7 @@ class MFAMixin:
class AuthPostCheckMixin:
@classmethod
def generate_reset_password_url_with_flash_msg(cls, user, message):
reset_passwd_url = reverse('authentication:reset-password')
@@ -319,20 +320,26 @@ class AuthPostCheckMixin:
@classmethod
def _check_passwd_is_too_simple(cls, user: User, password):
if password == 'admin' or password == 'ChangeMe':
if not user.is_auth_backend_model():
return
if user.check_passwd_too_simple(password):
message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
raise errors.PasswordTooSimple(url)
@classmethod
def _check_passwd_need_update(cls, user: User):
if user.need_update_password:
if not user.is_auth_backend_model():
return
if user.check_need_update_password():
message = _('You should to change your password before login')
url = cls.generate_reset_password_url_with_flash_msg(user, message)
raise errors.PasswordNeedUpdate(url)
@classmethod
def _check_password_require_reset_or_not(cls, user: User):
if not user.is_auth_backend_model():
return
if user.password_has_expired:
message = _('Your password has expired, please reset before logging in')
url = cls.generate_reset_password_url_with_flash_msg(user, message)

View File

@@ -2,13 +2,19 @@
#
from celery import shared_task
from ops.celery.decorator import register_as_period_task
from django.contrib.sessions.models import Session
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from ops.celery.decorator import register_as_period_task
@shared_task(verbose_name=_('Clean expired session'))
@shared_task(
verbose_name=_('Clean expired session'),
description=_(
"Since user logins create sessions, the system will clean up expired sessions every 24 hours"
)
)
@register_as_period_task(interval=3600 * 24)
def clean_django_sessions():
Session.objects.filter(expire_date__lt=timezone.now()).delete()

View File

@@ -8,10 +8,8 @@
<p>
<b>{% trans 'Username' %}:</b> {{ username }}<br>
<b>{% trans 'Login Date' %}:</b> {{ time }}<br>
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})<br>
</p>
-
<p>
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
</p>

View File

@@ -10,8 +10,7 @@
{% trans 'Click here reset password' %}
</a>
</p>
-
<br>
<p>
{% trans 'This link is valid for 1 hour. After it expires' %}
<a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>

View File

@@ -5,10 +5,9 @@
{% trans 'Your password has just been successfully updated' %}
</p>
<p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
<b>{% trans 'Browser' %}:</b> {{ browser }}
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
<b>{% trans 'Browser' %}:</b> {{ browser }} <br>
</p>
-
<p>
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br/>
{% trans 'If you have any questions, you can contact the administrator' %}

View File

@@ -5,10 +5,9 @@
{% trans 'Your public key has just been successfully updated' %}
</p>
<p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
<b>{% trans 'Browser' %}:</b> {{ browser }}
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
<b>{% trans 'Browser' %}:</b> {{ browser }} <br>
</p>
-
<p>
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br/>
{% trans 'If you have any questions, you can contact the administrator' %}

View File

@@ -1,4 +1,4 @@
from .utils import gen_key_pair, rsa_decrypt, rsa_encrypt
from common.utils import gen_key_pair, rsa_decrypt, rsa_encrypt
def test_rsa_encrypt_decrypt(message='test-password-$%^&*'):

View File

@@ -38,9 +38,15 @@ class SuggestionMixin:
class RenderToJsonMixin:
@action(methods=[POST, PUT], detail=False, url_path='render-to-json')
def render_to_json(self, request: Request, *args, **kwargs):
rows = request.data
if rows and isinstance(rows[0], dict):
first = list(rows[0].values())[0]
if first.startswith('#Help'):
rows.pop(0)
data = {
'title': (),
'data': request.data,
'data': rows,
}
jms_context = getattr(request, 'jms_context', {})

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals
import os
import sys
from django.apps import AppConfig
@@ -12,8 +13,11 @@ class CommonConfig(AppConfig):
from . import signal_handlers # noqa
from . import tasks # noqa
from .signals import django_ready
excludes = ['migrate', 'compilemessages', 'makemigrations']
for i in excludes:
if i in sys.argv:
return
django_ready.send(CommonConfig)
if not os.environ.get('DJANGO_DEBUG_SHELL'):
django_ready.send(CommonConfig)

View File

@@ -1,5 +1,6 @@
CRONTAB_AT_AM_TWO = '0 2 * * *'
CRONTAB_AT_AM_THREE = '0 3 * * *'
CRONTAB_AT_AM_TEN = '0 10 * * *'
CRONTAB_AT_PM_TWO = '0 14 * * *'

View File

@@ -108,7 +108,7 @@ class BaseFileParser(BaseParser):
if not matched:
return v
obj_name, obj_id = matched.groups()
if len(obj_id) < 36:
if obj_id.isdigit():
obj_id = int(obj_id)
return {'pk': obj_id, 'name': obj_name}
@@ -119,8 +119,6 @@ class BaseFileParser(BaseParser):
value = field.to_file_internal_value(value)
elif isinstance(field, serializers.BooleanField):
value = value.lower() in ['true', '1', 'yes']
elif isinstance(field, serializers.ChoiceField):
value = value
elif isinstance(field, ObjectRelatedField):
if field.many:
value = [self.id_name_to_obj(v) for v in value]
@@ -164,6 +162,15 @@ class BaseFileParser(BaseParser):
data.append(row_data)
return data
@staticmethod
def pop_help_text_if_need(rows):
rows = list(rows)
if not rows:
return rows
if rows[0][0].startswith('#Help'):
rows.pop(0)
return rows
def parse(self, stream, media_type=None, parser_context=None):
assert parser_context is not None, '`parser_context` should not be `None`'
@@ -192,6 +199,7 @@ class BaseFileParser(BaseParser):
request.jms_context = {}
request.jms_context['column_title_field_pairs'] = column_title_field_pairs
rows = self.pop_help_text_if_need(rows)
data = self.generate_data(field_names, rows)
return data
except Exception as e:

View File

@@ -5,12 +5,13 @@ from datetime import datetime
import pyzipper
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.renderers import BaseRenderer
from rest_framework.utils import encoders, json
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.serializers import fields as common_fields
from common.utils import get_logger
logger = get_logger(__file__)
@@ -38,24 +39,27 @@ class BaseFileRenderer(BaseRenderer):
filename_prefix = serializer.Meta.model.__name__.lower()
else:
filename_prefix = 'download'
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = "{}_{}.{}".format(filename_prefix, now, self.format)
suffix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if self.template == 'import':
suffix = 'template'
filename = "{}_{}.{}".format(filename_prefix, suffix, self.format)
disposition = 'attachment; filename="{}"'.format(filename)
response['Content-Disposition'] = disposition
def get_rendered_fields(self):
fields = self.serializer.fields
meta = getattr(self.serializer, 'Meta', None)
if self.template == 'import':
fields = [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id']
fields_unimport = getattr(meta, 'fields_unimport_template', [])
fields = [v for v in fields if v.field_name not in fields_unimport]
elif self.template == 'update':
fields = [v for k, v in fields.items() if not v.read_only and k != "org_id"]
else:
fields = [v for k, v in fields.items() if not v.write_only and k != "org_id"]
meta = getattr(self.serializer, 'Meta', None)
if meta:
fields_unexport = getattr(meta, 'fields_unexport', [])
fields = [v for v in fields if v.field_name not in fields_unexport]
fields_unexport = getattr(meta, 'fields_unexport', [])
fields = [v for v in fields if v.field_name not in fields_unexport]
return fields
@staticmethod
@@ -105,10 +109,10 @@ class BaseFileRenderer(BaseRenderer):
value = field.to_file_representation(value)
elif isinstance(value, bool):
value = 'Yes' if value else 'No'
elif isinstance(field, LabeledChoiceField):
elif isinstance(field, common_fields.LabeledChoiceField):
value = value or {}
value = '{}({})'.format(value.get('label'), value.get('value'))
elif isinstance(field, ObjectRelatedField):
elif isinstance(field, common_fields.ObjectRelatedField):
if field.many:
value = [self.to_id_name(v) for v in value]
else:
@@ -126,6 +130,53 @@ class BaseFileRenderer(BaseRenderer):
value = json.dumps(value, cls=encoders.JSONEncoder, ensure_ascii=False)
return str(value)
def get_field_help_text(self, field):
text = ''
if hasattr(field, 'get_render_help_text'):
text = field.get_render_help_text()
elif isinstance(field, serializers.BooleanField):
text = _('Yes/No')
elif isinstance(field, serializers.CharField):
if field.max_length:
text = _('Text, max length {}').format(field.max_length)
else:
text = _("Long text, no length limit")
elif isinstance(field, serializers.IntegerField):
text = _('Number, min {} max {}').format(field.min_value, field.max_value)
text = text.replace('min None', '').replace('max None', '')
elif isinstance(field, serializers.DateTimeField):
text = _('Datetime format {}').format(timezone.now().strftime(settings.REST_FRAMEWORK['DATETIME_FORMAT']))
elif isinstance(field, serializers.IPAddressField):
text = _('IP')
elif isinstance(field, serializers.ChoiceField):
choices = [str(v) for v in field.choices.keys()]
if isinstance(field, common_fields.LabeledChoiceField):
text = _("Choices, format name(value), name is optional for human read,"
" value is requisite, options {}").format(','.join(choices))
else:
text = _("Choices, options {}").format(",".join(choices))
elif isinstance(field, common_fields.PhoneField):
text = _("Phone number, format +8612345678901")
elif isinstance(field, common_fields.LabeledChoiceField):
text = _('Label, format ["key:value"]')
elif isinstance(field, common_fields.ObjectRelatedField):
text = _("Object, format name(id), name is optional for human read, id is requisite")
elif isinstance(field, serializers.PrimaryKeyRelatedField):
text = _('Object, format id')
elif isinstance(field, serializers.ManyRelatedField):
child_relation_class_name = field.child_relation.__class__.__name__
if child_relation_class_name == "ObjectRelatedField":
text = _('Objects, format ["name(id)", ...], name is optional for human read, id is requisite')
elif child_relation_class_name == "LabelRelatedField":
text = _('Labels, format ["key:value", ...], if label not exists, will create it')
else:
text = _('Objects, format ["id", ...]')
elif isinstance(field, serializers.ListSerializer):
child = field.child
if hasattr(child, 'get_render_help_text'):
text = child.get_render_help_text()
return text
def generate_rows(self, data, render_fields):
for item in data:
row = []
@@ -135,6 +186,17 @@ class BaseFileRenderer(BaseRenderer):
row.append(value)
yield row
def write_help_text_if_need(self):
if self.template == 'export':
return
fields = self.get_rendered_fields()
row = []
for f in fields:
text = self.get_field_help_text(f)
row.append(text)
row[0] = '#Help ' + str(row[0])
self.write_row(row)
@abc.abstractmethod
def initial_writer(self):
raise NotImplementedError
@@ -184,6 +246,7 @@ class BaseFileRenderer(BaseRenderer):
rows = self.generate_rows(data, rendered_fields)
self.initial_writer()
self.write_column_titles(column_titles)
self.write_help_text_if_need()
self.write_rows(rows)
self.after_render()
value = self.get_rendered_value()

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