mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-04-27 11:12:54 +00:00
Compare commits
1202 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a9d455e867 | ||
|
d06d26ac54 | ||
|
e992c44e11 | ||
|
24fe058fd9 | ||
|
a3fef9cc54 | ||
|
471053e62a | ||
|
dc6308b030 | ||
|
f016ae6161 | ||
|
14a8d877e0 | ||
|
ddf20570a1 | ||
|
1ad9616b7f | ||
|
d7bc6bb201 | ||
|
f855043468 | ||
|
3159a4e794 | ||
|
57fcebfdd3 | ||
|
c500bb4e4c | ||
|
fd062b0da6 | ||
|
bcb112d5c6 | ||
|
533dbf316c | ||
|
9cce94b709 | ||
|
8b815d812b | ||
|
a168fc8a62 | ||
|
faae1a09d1 | ||
|
26e819e120 | ||
|
79579654a1 | ||
|
6bc1c5bd50 | ||
|
36f312b943 | ||
|
11811c453b | ||
|
12fadeec58 | ||
|
b49fd21e08 | ||
|
9b982eb592 | ||
|
31652ef5b1 | ||
|
8fef18b991 | ||
|
c804c053d2 | ||
|
bef2282604 | ||
|
cabc069045 | ||
|
99c9a021b7 | ||
|
6cb3cc1f29 | ||
|
67422ef4ba | ||
|
3d6d2af268 | ||
|
ee97e45cc3 | ||
|
0131eaa6db | ||
|
eaa390fd6f | ||
|
e2b8fd0d40 | ||
|
2aace05099 | ||
|
1ee70af93d | ||
|
fa70fb2921 | ||
|
01a6019022 | ||
|
5c61a11d82 | ||
|
67f3341310 | ||
|
cb49e26387 | ||
|
314da330c0 | ||
|
f1c98fda34 | ||
|
1fdd1036d3 | ||
|
e286997090 | ||
|
ce3daf5496 | ||
|
631570b819 | ||
|
9b1bff0847 | ||
|
ee8a2afe16 | ||
|
1a01c0537c | ||
|
64393fe695 | ||
|
11ef4fab4e | ||
|
9f8256f885 | ||
|
5390fbacec | ||
|
8b9fe3c72b | ||
|
20070e0647 | ||
|
47b72cb35e | ||
|
2ca0e9a5a2 | ||
|
3b2ac101c8 | ||
|
6795f036dd | ||
|
aaa1f48258 | ||
|
53c5bab203 | ||
|
1254d28463 | ||
|
d6b1a577fc | ||
|
5ab85d3561 | ||
|
467f4c5d4f | ||
|
f2404319af | ||
|
bbeadf7dbe | ||
|
941bd9b3f4 | ||
|
37a307a9d0 | ||
|
528f9045d0 | ||
|
a317549a01 | ||
|
0f5681de7d | ||
|
a7c514f8d8 | ||
|
75ea0079a2 | ||
|
4cc1687bf8 | ||
|
76e57b9a3e | ||
|
ba3bce1e2e | ||
|
45f0343cfa | ||
|
acaa4cf2d5 | ||
|
3f452daee8 | ||
|
5e25361ee8 | ||
|
7b7604e14d | ||
|
f9037878c3 | ||
|
29ddfcac17 | ||
|
519ec65ad4 | ||
|
1f60e328b6 | ||
|
e8e0ea920b | ||
|
4fd8efd043 | ||
|
623c800d31 | ||
|
d2c6e3c7a6 | ||
|
dc5883576d | ||
|
0a9c9fb227 | ||
|
15a1a58eca | ||
|
782401ef86 | ||
|
8abcd201bc | ||
|
cdbc10ac72 | ||
|
ceeef890e6 | ||
|
dc8a172884 | ||
|
62115e43bb | ||
|
5eced85e69 | ||
|
ec99b17b76 | ||
|
84569720c3 | ||
|
65984d38f1 | ||
|
f6913ac63c | ||
|
514b2cdfc5 | ||
|
b55000663e | ||
|
9ed822bb3e | ||
|
ea599d7695 | ||
|
01c5d68b35 | ||
|
2e2c331941 | ||
|
266ea9b858 | ||
|
5f2e838342 | ||
|
544ad5532b | ||
|
d22d715ee7 | ||
|
dd2366532c | ||
|
9667a3d340 | ||
|
c8e6e5d38c | ||
|
9d1047fae2 | ||
|
28f97d746d | ||
|
be72344c63 | ||
|
d3176b68a8 | ||
|
5411f65546 | ||
|
e3ba468004 | ||
|
a03a11efa4 | ||
|
d344495417 | ||
|
9412bd0331 | ||
|
8d73ddb1cd | ||
|
7fe56a5e1a | ||
|
db259d4e8b | ||
|
afc31ee5ce | ||
|
69cc47e0cb | ||
|
2455afc2d2 | ||
|
33cb793c19 | ||
|
5522ba0241 | ||
|
0fed338277 | ||
|
8f78919b3e | ||
|
f6fc9b1f5c | ||
|
365dbf6e14 | ||
|
e5a5d5f727 | ||
|
e0d6b843ee | ||
|
2a31a7d444 | ||
|
e68d5564c6 | ||
|
4fdb049c9d | ||
|
e5f66c4be2 | ||
|
325edfe704 | ||
|
3a93aeb155 | ||
|
28d6f2f9ef | ||
|
2bfcebd064 | ||
|
6f2cb9b3c6 | ||
|
cdebfd8121 | ||
|
57d05e6ff0 | ||
|
49378d1f13 | ||
|
e802e145af | ||
|
2faad88b78 | ||
|
33a47139b3 | ||
|
50faa3242a | ||
|
d89164db63 | ||
|
f72fc19ba6 | ||
|
7bd03c7863 | ||
|
ed95a89a77 | ||
|
428a4470c9 | ||
|
b206e751da | ||
|
6913518046 | ||
|
f5a2f5e538 | ||
|
f4fa153ffa | ||
|
c70d7f709f | ||
|
ec95144907 | ||
|
424ef4d9a5 | ||
|
40bbc4a02c | ||
|
a784ca29c3 | ||
|
b7820c6a5b | ||
|
7bac1b42e4 | ||
|
0a6757946a | ||
|
e4d169cabe | ||
|
c80ca5236d | ||
|
f513eb62a6 | ||
|
94e8c62953 | ||
|
bd783e6a8d | ||
|
35bc3a0e2b | ||
|
996bee3afd | ||
|
7c4931b6af | ||
|
9992fb35be | ||
|
cc63c956cb | ||
|
15919085bc | ||
|
9e4b82bf45 | ||
|
28f85a0186 | ||
|
30e64ecfc1 | ||
|
042c3d1ba8 | ||
|
94a8122eac | ||
|
88450d796f | ||
|
3d28e255c0 | ||
|
153be1508f | ||
|
6e83420e67 | ||
|
e03d983020 | ||
|
0afc1e6f5b | ||
|
a9ea801862 | ||
|
23f9b79142 | ||
|
4db15d9af7 | ||
|
bc9782bd55 | ||
|
635e9f5079 | ||
|
71259886fe | ||
|
a26cc7ce1f | ||
|
a2aa5e9bf9 | ||
|
82de373f8e | ||
|
59e7778e4a | ||
|
60eaec68c6 | ||
|
43973122bf | ||
|
8668955d4a | ||
|
5571fb6f42 | ||
|
62b9b6883e | ||
|
7af1c6a2bb | ||
|
3e96c2fe79 | ||
|
3a1d3c1f5c | ||
|
9466c7105c | ||
|
a3467bdabc | ||
|
4c547215aa | ||
|
2580e7a712 | ||
|
038d93e318 | ||
|
f46cc0f040 | ||
|
423d6db2ac | ||
|
243083e876 | ||
|
c84bc52c70 | ||
|
090ad0ba83 | ||
|
033750f108 | ||
|
405344de74 | ||
|
763e67bd1d | ||
|
e8c581b08a | ||
|
47029be3da | ||
|
bc70c480f7 | ||
|
d2d9d3d841 | ||
|
56d0a6d0b0 | ||
|
060e8ace70 | ||
|
821622638a | ||
|
51c67f0e9d | ||
|
70c8db839e | ||
|
c9208d2cf7 | ||
|
58177fdfb0 | ||
|
76dd2f8153 | ||
|
edd998da20 | ||
|
8b2276ce08 | ||
|
f42f46ffe4 | ||
|
b2f8f9d248 | ||
|
8105681304 | ||
|
b4d3f6099a | ||
|
9b48da11dc | ||
|
904154a62b | ||
|
7181dad5ad | ||
|
2a250d13d8 | ||
|
caa1e2e1ac | ||
|
5b044299b9 | ||
|
d48a12e266 | ||
|
35ec9dc2f4 | ||
|
33f3281a1f | ||
|
93810fb0db | ||
|
8fc2b86189 | ||
|
6217733aba | ||
|
4b4d7b6787 | ||
|
acbc3e1e44 | ||
|
110b3a334d | ||
|
a498b22e80 | ||
|
385bf47b11 | ||
|
16400082e7 | ||
|
cbc009cb3f | ||
|
919cdeae20 | ||
|
456b96a369 | ||
|
ff9ad2680f | ||
|
96a0cbc35d | ||
|
08963ebb40 | ||
|
908e6cb81a | ||
|
c206f5d09c | ||
|
f0c7a7508b | ||
|
d386189c77 | ||
|
f0fa381b9f | ||
|
8b2af5ee0a | ||
|
0541c0a9d4 | ||
|
e4c7eb8035 | ||
|
e33aeaa338 | ||
|
1282fffff7 | ||
|
3f4141ca0b | ||
|
d516349a68 | ||
|
8730fa8dee | ||
|
9a5a775652 | ||
|
8304ae9070 | ||
|
9533861e24 | ||
|
abbfbcde83 | ||
|
046a9d41bf | ||
|
363bb20da7 | ||
|
2b7c8b9c07 | ||
|
db040bbd06 | ||
|
a761ec9aa1 | ||
|
c0ffe45ce9 | ||
|
404d58a9c9 | ||
|
f64eab7a15 | ||
|
46f94fd138 | ||
|
2f1c0090b7 | ||
|
b0d6a09276 | ||
|
d8db76cc7b | ||
|
b35a55ed54 | ||
|
dc5ecfcc4b | ||
|
594d5b8128 | ||
|
543dde57ab | ||
|
c088437fe5 | ||
|
e721ec147c | ||
|
5d18d6dee0 | ||
|
ecfd338428 | ||
|
4b28b079dc | ||
|
c1c3236a30 | ||
|
4b19750581 | ||
|
eafb5ecfb3 | ||
|
583486e26e | ||
|
8198620a2e | ||
|
c0b301d52b | ||
|
7791d6222a | ||
|
b740d9d42f | ||
|
48d0187604 | ||
|
6217018427 | ||
|
923f40e523 | ||
|
1f1fe2084b | ||
|
b8b1a6ac9c | ||
|
35f88722af | ||
|
7e6d2749ae | ||
|
be57b101ff | ||
|
41c8cb6307 | ||
|
3a7ae01ede | ||
|
d17ca4f6a7 | ||
|
5a14bb13d0 | ||
|
2956f2e4b7 | ||
|
e983ac3cbc | ||
|
fab156dc5f | ||
|
f6f897317e | ||
|
a0441cd6ea | ||
|
e9abd1e72d | ||
|
9fcb4ecba0 | ||
|
4b637ad86e | ||
|
829f867962 | ||
|
7f965b55f4 | ||
|
0e0be618e5 | ||
|
9577af3221 | ||
|
a6b7cc9d1b | ||
|
7a9a71197a | ||
|
3cd68ba0a9 | ||
|
02bdd0f07d | ||
|
98cf6f82b7 | ||
|
27fd5d51b9 | ||
|
095ca91e30 | ||
|
d05514962a | ||
|
c4066a03fa | ||
|
a7d4c4ca2a | ||
|
5b0f8f63a3 | ||
|
c4bcae68bf | ||
|
29ca50f97e | ||
|
49aaf8d53e | ||
|
931e15173b | ||
|
4018a59b2e | ||
|
88905bd28d | ||
|
abad98a190 | ||
|
7419139b29 | ||
|
a1fd3b1ecb | ||
|
8a8a7f9947 | ||
|
f9e6fc98fb | ||
|
0dd015bcba | ||
|
d1ea31c9a4 | ||
|
e2bf56e624 | ||
|
26040a5560 | ||
|
54726f0a2d | ||
|
7fd88b95f9 | ||
|
4f271d6405 | ||
|
fe17a8c3a0 | ||
|
ee5e97e860 | ||
|
dddfc66efd | ||
|
d005bd804f | ||
|
08de04fdbc | ||
|
9ed7c41514 | ||
|
1a81b76a46 | ||
|
cf99a7a031 | ||
|
64551b13a1 | ||
|
c715300416 | ||
|
d9031ae02b | ||
|
0d2ba5c518 | ||
|
817957dbac | ||
|
3796af78a6 | ||
|
1191e4ab2d | ||
|
1c6fcc5826 | ||
|
4728f95634 | ||
|
013502186b | ||
|
a6d040cd34 | ||
|
398758baa6 | ||
|
e29bddd89e | ||
|
e35c915ee3 | ||
|
de2dd583d0 | ||
|
43f1d7eeae | ||
|
9bb63e0933 | ||
|
c9e03fd5d8 | ||
|
7a147242c9 | ||
|
392c261a96 | ||
|
2bbccae0f5 | ||
|
606fa9bfbc | ||
|
96e7b165dd | ||
|
148413d280 | ||
|
a46a81d477 | ||
|
ff0f9eb6eb | ||
|
d8dfaf0868 | ||
|
3267c8074b | ||
|
7b14d680b2 | ||
|
0980808bb7 | ||
|
0519f15bbf | ||
|
f6742eb4c6 | ||
|
f8d11013fc | ||
|
7875777ed1 | ||
|
0ca81a8f30 | ||
|
09accbd922 | ||
|
945204c45b | ||
|
2d62dc0657 | ||
|
fa61688c28 | ||
|
801edc7cc9 | ||
|
d0617a0ea4 | ||
|
1191ed1793 | ||
|
4036420d0e | ||
|
35a1655905 | ||
|
d4dc31aefa | ||
|
04ec34364f | ||
|
01b8c1f7a8 | ||
|
77598a0f23 | ||
|
eafb074fda | ||
|
d4d903f5c6 | ||
|
c9c55b5fcb | ||
|
f7313bfcc1 | ||
|
d2f7376f78 | ||
|
6db56eb2aa | ||
|
442290703a | ||
|
e491a724ed | ||
|
230924baac | ||
|
0ae2f04f28 | ||
|
68a490d305 | ||
|
6abfeee683 | ||
|
1a03f7b265 | ||
|
2dae2b3789 | ||
|
bdbbebab76 | ||
|
33170887f4 | ||
|
88302c8846 | ||
|
4068b5c76a | ||
|
9966ad4c71 | ||
|
9cfe974c52 | ||
|
d9a9f890f5 | ||
|
e2904ab042 | ||
|
f92c557235 | ||
|
cfadbc164c | ||
|
374a102bc4 | ||
|
84e1411c22 | ||
|
e28bf170d1 | ||
|
7c9e3a1362 | ||
|
fba80342a5 | ||
|
5eeff0aabf | ||
|
5b4de02fff | ||
|
b6a5854fa2 | ||
|
9771d3c817 | ||
|
b33a0cf0b1 | ||
|
f9fa6ad9c1 | ||
|
4b2db2b6a1 | ||
|
822b353a40 | ||
|
2908d4ee5f | ||
|
482c4ced0c | ||
|
b2a5e457a9 | ||
|
343c3607fa | ||
|
f03263eedf | ||
|
98d7ecbf3e | ||
|
477ccda8ca | ||
|
fcdc2b9510 | ||
|
1ee57cfda0 | ||
|
804bd289a4 | ||
|
86273865c8 | ||
|
5142f0340c | ||
|
7c80c52d02 | ||
|
eb30b61ca9 | ||
|
dd5a272cdf | ||
|
5b27acf4ef | ||
|
1a41a7450e | ||
|
e1b501c7d4 | ||
|
b660bfb7ff | ||
|
5724912480 | ||
|
11b3bafd5a | ||
|
9f90838df1 | ||
|
b01916001e | ||
|
c96ae1022b | ||
|
8f11167db0 | ||
|
a53397b76f | ||
|
8f13224454 | ||
|
8f4dd25e69 | ||
|
9c8762e3a0 | ||
|
a8cf788122 | ||
|
7355a4f152 | ||
|
2cf80e6615 | ||
|
9a18ed631c | ||
|
1e16f1cb9f | ||
|
35b8b080ab | ||
|
4219d54db3 | ||
|
c3620254b3 | ||
|
d30de0b6a0 | ||
|
af91b6faeb | ||
|
49b84b019d | ||
|
a0ee520572 | ||
|
972afe0bfe | ||
|
e47e9b0a11 | ||
|
87e54d8823 | ||
|
a73c8d8285 | ||
|
b0dd8d044d | ||
|
7c55c42582 | ||
|
cc1fcd2b98 | ||
|
8434d8d5ba | ||
|
044fd238b8 | ||
|
be096a1319 | ||
|
6fa14833b3 | ||
|
1f32ab274c | ||
|
b0f86e43a6 | ||
|
9b0c81333f | ||
|
05fc966444 | ||
|
b87650038f | ||
|
d4f69a7ff8 | ||
|
0e1e26c29c | ||
|
1b8cdbc4dd | ||
|
2a781c228f | ||
|
35d6b0f16a | ||
|
ca8987fef6 | ||
|
b385133071 | ||
|
aa78a03efa | ||
|
31f8a19392 | ||
|
7a528b499a | ||
|
1c6ce422cf | ||
|
f9cf2ea2e5 | ||
|
575b3a617f | ||
|
b7362d3f51 | ||
|
6ee3860124 | ||
|
7e111da529 | ||
|
578458f734 | ||
|
bd56697d6d | ||
|
aad824d127 | ||
|
63f828da0b | ||
|
7c211b3fb6 | ||
|
3881edd2ba | ||
|
b882b12d04 | ||
|
addd2e7d1c | ||
|
ad6d2e1cd7 | ||
|
5f07271afa | ||
|
efdcd4c708 | ||
|
b62763bca3 | ||
|
e95da730f2 | ||
|
43fa3f420a | ||
|
0311446384 | ||
|
f7030e4fee | ||
|
fce8cc375f | ||
|
920199c6df | ||
|
d09eb3c4fa | ||
|
6e8affcdd6 | ||
|
647736f4e3 | ||
|
cbc09d84df | ||
|
4c957dd03b | ||
|
d34b65890f | ||
|
b53968ac00 | ||
|
f2ccb15101 | ||
|
db5bf046fc | ||
|
59c87483e6 | ||
|
26420b78f8 | ||
|
e47bdc093e | ||
|
3dde80a60a | ||
|
e373a79d63 | ||
|
744a5cd0e3 | ||
|
37ca4a46ee | ||
|
0dc9214f98 | ||
|
513508654b | ||
|
ef2b12fa0f | ||
|
4e719ecacd | ||
|
755a124b50 | ||
|
d6888776e7 | ||
|
29e233e715 | ||
|
99c3696d96 | ||
|
ed6de83e8c | ||
|
134f1a440c | ||
|
7da82242fe | ||
|
2fd50d2425 | ||
|
41a3e89248 | ||
|
b125297c37 | ||
|
24255b69ee | ||
|
3bb51b39c4 | ||
|
b54da7d3b3 | ||
|
534af0abf0 | ||
|
8b0073333b | ||
|
d8af2274f4 | ||
|
3dd828d703 | ||
|
fa6b4a5b63 | ||
|
8bd86c77f9 | ||
|
3828e89cf8 | ||
|
e531b040ef | ||
|
3eee84a34e | ||
|
ab29df5991 | ||
|
b042f00688 | ||
|
5beebaf51c | ||
|
50f075cc7e | ||
|
e997236159 | ||
|
c8b1d892e3 | ||
|
9cb9e7328b | ||
|
85129da942 | ||
|
1cb00b1db4 | ||
|
c3798bfa95 | ||
|
1d280599ae | ||
|
ee8d7cdcac | ||
|
1b4114fd5f | ||
|
3c6c476f2e | ||
|
f19e3fedbd | ||
|
542e64278f | ||
|
cd76294e81 | ||
|
4f9158b2ad | ||
|
e319f20296 | ||
|
b00f3a851c | ||
|
ab529fd22c | ||
|
c2784c44ad | ||
|
512e727ac6 | ||
|
2dd0154967 | ||
|
f55869a449 | ||
|
b6f3c23787 | ||
|
6982ab1efc | ||
|
db4d841bb0 | ||
|
ef91ebb468 | ||
|
6264319c51 | ||
|
1417abecfb | ||
|
bd548b3fe2 | ||
|
94cef9ea6e | ||
|
a338613b5a | ||
|
0d833a966c | ||
|
76b6489636 | ||
|
763fe778d5 | ||
|
cf1dc79c68 | ||
|
7973239424 | ||
|
1baacd0b2c | ||
|
054d385ffc | ||
|
50d3a4906a | ||
|
c8b7008d42 | ||
|
e94520a3fd | ||
|
55e8e34226 | ||
|
8755ece633 | ||
|
c545e2a3aa | ||
|
1068662ab1 | ||
|
75141741a1 | ||
|
9da507bb62 | ||
|
160293365a | ||
|
7a19007aba | ||
|
f866b93f96 | ||
|
b9e64747ac | ||
|
25a473dc99 | ||
|
e3bf015aa9 | ||
|
6d3d4a08af | ||
|
9554de4ea6 | ||
|
6157ff7b7d | ||
|
774fd176fd | ||
|
b489db8054 | ||
|
6b9fa6e01f | ||
|
9b59954393 | ||
|
ecaf19563f | ||
|
c431e96eaf | ||
|
d86f241450 | ||
|
3252db31fe | ||
|
dac118dd26 | ||
|
181eb621c0 | ||
|
828582333d | ||
|
657f7f822b | ||
|
93627e4f9d | ||
|
2adb2519fa | ||
|
32ec48ac14 | ||
|
b3a0d81740 | ||
|
2b160fbbc2 | ||
|
60fcf5fcd3 | ||
|
7c2e50435d | ||
|
57f91d0973 | ||
|
49c033e003 | ||
|
6476a8fee8 | ||
|
c10db2ab0f | ||
|
647beffc01 | ||
|
ac0c6ef3d5 | ||
|
e13741827d | ||
|
29caf0154e | ||
|
fbdcc437e6 | ||
|
b38e5df1aa | ||
|
0a39ba0a75 | ||
|
c56e1bdbbe | ||
|
6b00ba271f | ||
|
bddb1de2f8 | ||
|
32ae77c42d | ||
|
3b1701b1aa | ||
|
3b9bcc719e | ||
|
8e6aa4524d | ||
|
cea63e6083 | ||
|
5d2d8ca487 | ||
|
81146f44f7 | ||
|
9adaa27f6c | ||
|
01c565f93f | ||
|
cb97afffab | ||
|
1b55bf1670 | ||
|
b1c68165bb | ||
|
5d3e633e83 | ||
|
c863bf63b1 | ||
|
c71a6ae4ba | ||
|
38e3d9de8b | ||
|
0c73acd4b9 | ||
|
581a5c73a6 | ||
|
e1ed1d7c4c | ||
|
805e7d1d5f | ||
|
1957c2983b | ||
|
6b1ceae6c5 | ||
|
2a5c41dfaf | ||
|
7a38c9136e | ||
|
9a3fdf76fc | ||
|
136db61011 | ||
|
0d338f80c5 | ||
|
bd3909ad27 | ||
|
96399f8315 | ||
|
4e90d17484 | ||
|
13de75c41f | ||
|
a77ebc5fee | ||
|
99ce82a6a0 | ||
|
ec95d25704 | ||
|
7c6e83d124 | ||
|
ad5e88f1e3 | ||
|
b1e958d806 | ||
|
8506ae9edd | ||
|
ceb2a9bb17 | ||
|
8d83c953d3 | ||
|
9825f9fbd2 | ||
|
41b2ce06a8 | ||
|
920cfdac5c | ||
|
8abf7876cc | ||
|
2e625f2c33 | ||
|
88037b2038 | ||
|
457021040a | ||
|
4887b21d35 | ||
|
03a66fd563 | ||
|
ef656a8dfd | ||
|
5e45129e32 | ||
|
ea64b01da6 | ||
|
c3b863c2be | ||
|
6a7896b712 | ||
|
83c1f8e4d3 | ||
|
9d3fdd37a3 | ||
|
419195895e | ||
|
c92188887d | ||
|
dcfc4e6e7b | ||
|
836adab5d0 | ||
|
e93227a53c | ||
|
d6f6bb9c1b | ||
|
85825165fc | ||
|
66047c7926 | ||
|
456bcd2d3f | ||
|
259f68a806 | ||
|
4e6231ab19 | ||
|
d7bbfdcce6 | ||
|
a0cc9e5db5 | ||
|
ea6cd853de | ||
|
53a388a7e0 | ||
|
13b1938efb | ||
|
6677985e4a | ||
|
cfa1034161 | ||
|
815973fb63 | ||
|
92d369aaca | ||
|
281a2d9679 | ||
|
e9f4615caa | ||
|
c0d2efa72a | ||
|
247f4d5c19 | ||
|
29c29b17d4 | ||
|
5608f7d20d | ||
|
aa8ae36255 | ||
|
2292e6f2eb | ||
|
bf82a1c721 | ||
|
8ef84bbc03 | ||
|
e36d51cc0b | ||
|
5c1d0238e1 | ||
|
c6befe4c4b | ||
|
5a57c296a1 | ||
|
34ddfd24be | ||
|
39051ef0fd | ||
|
ddd813241c | ||
|
60f7cbef9a | ||
|
4adc981a21 | ||
|
c42913c15e | ||
|
bb6d60b46d | ||
|
afe7f03c16 | ||
|
ba8d3be9a6 | ||
|
d14d8869ac | ||
|
2f7391efc3 | ||
|
75fa96b29c | ||
|
c56ab9bc1e | ||
|
443e492fd4 | ||
|
b8c223d525 | ||
|
a509afe24b | ||
|
9654add528 | ||
|
d0a9409078 | ||
|
5836583490 | ||
|
57d689bee6 | ||
|
8a3fb6bd4d | ||
|
78bd3f581a | ||
|
d07c476507 | ||
|
50d196eda4 | ||
|
823d9af91d | ||
|
3731123369 | ||
|
1a68c4b44a | ||
|
0f79006b59 | ||
|
c95ad5a31c | ||
|
e25a96d359 | ||
|
04284adc87 | ||
|
7ee7d50f22 | ||
|
3d015398c3 | ||
|
da8b328f80 | ||
|
82a6702c90 | ||
|
ad267bcd35 | ||
|
15dc922bca | ||
|
22405d46d6 | ||
|
35b0741068 | ||
|
d7b8174fd0 | ||
|
43cfb11bca | ||
|
f955cebaa0 | ||
|
5d7ec054e6 | ||
|
6088a38eed | ||
|
e1a84e76bb | ||
|
19f9179e7f | ||
|
aa4a8d5b42 | ||
|
10ba31086c | ||
|
fa8312bc65 | ||
|
512e727dd4 | ||
|
a529609275 | ||
|
a8973330fe | ||
|
d42acc3848 | ||
|
912cefbc85 | ||
|
2bb475d0ce | ||
|
22788ff2da | ||
|
5594b25ae0 | ||
|
4733d89807 | ||
|
c718fe1a9d | ||
|
237b4a82c9 | ||
|
76e0cbb8ac | ||
|
b3a670d380 | ||
|
db243d050e | ||
|
cd2648291e | ||
|
4a49bde1f0 | ||
|
d9754496d0 | ||
|
6753b5fd19 | ||
|
aeb320ba30 | ||
|
e712e8ccfc | ||
|
1d6f827296 | ||
|
772c9b385c | ||
|
f5053728e7 | ||
|
f67fd29499 | ||
|
138ea35620 | ||
|
bf56549f01 | ||
|
908181af64 | ||
|
7b4d3c44f8 | ||
|
b7a6454d65 | ||
|
6d81fa7fdf | ||
|
0e8833cce3 | ||
|
24d9e65532 | ||
|
bca9bdf619 | ||
|
cd39e20808 | ||
|
9c8680d3f4 | ||
|
dd84ca8f85 | ||
|
96c1f689c0 | ||
|
84855bfd7e | ||
|
40c5a218a9 | ||
|
8e87972a76 | ||
|
3faee9b80c | ||
|
5a1389a187 | ||
|
565c2f493c | ||
|
8d48593fc4 | ||
|
b50c96fcd6 | ||
|
85700a2a26 | ||
|
66615b7dd3 | ||
|
2c1a1fa31e | ||
|
bbc442b56e | ||
|
1ca579f4f0 | ||
|
9e3b23179c | ||
|
9fd861d047 | ||
|
4abfcb27d1 | ||
|
3463761693 | ||
|
c311adc1da | ||
|
ee258707c8 | ||
|
17d96669fe | ||
|
3fade107d5 | ||
|
f91ec6fa6a | ||
|
dfff41e9d6 | ||
|
478e81b8fa | ||
|
9b14f2aa1f | ||
|
18e648af6e | ||
|
45bd69585a | ||
|
42a0cde450 | ||
|
a9ef21ea3f | ||
|
13d24a12db | ||
|
2bd09f246d | ||
|
23c81cf5eb | ||
|
e95284335e | ||
|
1c7f82e65a | ||
|
dfde50c768 | ||
|
8bfbebf29e | ||
|
8157f9891f | ||
|
ad95adc833 | ||
|
f7e55c9b89 | ||
|
11b125655d | ||
|
c6628a1959 | ||
|
ae7dbbedcc | ||
|
407a77f61b | ||
|
e06f9a03d6 | ||
|
07edbea54e | ||
|
856e501a15 | ||
|
8cf900f9de | ||
|
a54605ac79 | ||
|
92790d711e | ||
|
5b548d8d57 | ||
|
afdf777386 | ||
|
cd2af0dcf7 | ||
|
523468f7af | ||
|
9385d04812 | ||
|
2ee435a8ec | ||
|
f3a827b76b | ||
|
50ceca9f06 | ||
|
8a5e86dfa7 | ||
|
6ffae48ab2 | ||
|
9ff78c8569 | ||
|
d6718d7b78 | ||
|
32966b260a | ||
|
6c59888d77 | ||
|
1c1d839b82 | ||
|
7d295cc675 | ||
|
75496cbe91 | ||
|
11f6a029de | ||
|
e40c66c7ed | ||
|
2a33337963 | ||
|
bd1a768743 | ||
|
0c0ec098ae | ||
|
37ad7b32e4 | ||
|
2640963938 | ||
|
6bc9181c25 | ||
|
d8379195e6 | ||
|
9195c658a0 | ||
|
3fb261b5c8 | ||
|
aa16c3d3a1 | ||
|
7be6cf2b73 | ||
|
60738da053 | ||
|
507ad10389 | ||
|
67bc16238c | ||
|
db88f6c9b4 | ||
|
8b7f60d43e | ||
|
cd1f6a9137 | ||
|
7973d066a3 | ||
|
ad65097a8f | ||
|
1b05f56598 | ||
|
3468f8cd40 | ||
|
5c81e974cd | ||
|
b638cf7417 | ||
|
1db1961cc0 | ||
|
811afdcf1a | ||
|
1f87ce2a47 | ||
|
8213e38e6a | ||
|
263bcbb566 | ||
|
050ddc88f2 | ||
|
38e8791d9f | ||
|
deb8474c1b | ||
|
12740ead08 | ||
|
6322559bd7 | ||
|
6c5eb00fb6 | ||
|
dad2f8eb65 | ||
|
c8679f48f5 | ||
|
510dc1eaf2 | ||
|
a313753757 | ||
|
53f106b30d | ||
|
0d27bfcfa9 | ||
|
ba6660216c | ||
|
3536af2051 | ||
|
21bb0a8162 | ||
|
d718398791 | ||
|
0b65e3ffda | ||
|
91a1da57e9 | ||
|
f95cbd6977 | ||
|
f16ec02c40 | ||
|
0ea2339ad5 | ||
|
8ebdd59e00 | ||
|
c4e30737a4 | ||
|
f127aca5f8 | ||
|
7333c8e094 | ||
|
a1e9382275 | ||
|
097a6c5c5f | ||
|
4e023057cc | ||
|
4034e2152c | ||
|
e8d6c6b711 | ||
|
43215d27c5 | ||
|
e20db96331 | ||
|
564ad40b99 | ||
|
32ef4c79da | ||
|
af4f6ebb26 | ||
|
33b688b021 | ||
|
b179770dbf | ||
|
e7f92ec0d7 | ||
|
79449a8a02 | ||
|
f259509ef8 | ||
|
82977f9023 | ||
|
4a5205c5ac | ||
|
714b4ef7f4 | ||
|
df091f0ee1 | ||
|
7037cf56ec | ||
|
f683d195e4 | ||
|
5ab55b823c | ||
|
0f2c769e8d | ||
|
1d53f292ae | ||
|
a15335cac9 | ||
|
f33cf07859 | ||
|
bce55421ce | ||
|
c3449cd6bc | ||
|
4e903ce19b | ||
|
90826b358c | ||
|
7d46aa9892 | ||
|
49d2bd93b7 | ||
|
9f103a88d6 | ||
|
ce33bdc370 | ||
|
cdf1f81c8a | ||
|
79edff5fca | ||
|
a7316bc7c1 | ||
|
09f802b00d | ||
|
a644b84bb1 | ||
|
b6f48111e3 | ||
|
3a6e4e7fb6 | ||
|
e42a98ff95 | ||
|
8fe511cec6 | ||
|
ffb3cd13cb | ||
|
b1abf8a339 | ||
|
89d20c8a4d | ||
|
d66f923c0c | ||
|
d3c14428a1 | ||
|
c104f85b18 | ||
|
755d8124ac | ||
|
a029cc8ed5 | ||
|
111dfa8c29 | ||
|
5f892c3afe | ||
|
313202fe41 | ||
|
af1adc3baa | ||
|
be214c84d1 | ||
|
082614e7b0 | ||
|
83835747c5 | ||
|
2a7b48c83d | ||
|
a9068496d9 | ||
|
8bad88e798 | ||
|
9f45eeeb1f | ||
|
60110982f1 | ||
|
259204bfe2 | ||
|
c55e9679db | ||
|
c05a3c315a | ||
|
dbdf586f5b | ||
|
b1bd4db3e9 | ||
|
7806a13db5 | ||
|
928f564109 | ||
|
328f718fe8 | ||
|
cb4402c610 | ||
|
fbc4cb9046 | ||
|
94567b86f0 | ||
|
8aa707427f | ||
|
7d64b8419f | ||
|
fad9249810 | ||
|
bb4fbc3a1c | ||
|
d7916a62f0 | ||
|
da27e1b93c | ||
|
99b24cad00 | ||
|
9dbdd6ac60 | ||
|
f8953441e3 | ||
|
5b41eddacc | ||
|
a432af1a6d | ||
|
f987515b89 | ||
|
2afabd65f9 | ||
|
85cb80cbfe | ||
|
8500f186f6 | ||
|
97f60a61e0 | ||
|
08ac8b0857 | ||
|
255817f5c6 | ||
|
19b196eb1f | ||
|
40db6485dd | ||
|
b23e99885e | ||
|
22fbbb92da | ||
|
99c94166bb | ||
|
169254a1c7 | ||
|
bda6037b2a | ||
|
1cf0b15528 | ||
|
ff3865d1a7 | ||
|
10435788bc | ||
|
02750e56d9 | ||
|
a1d53cba44 | ||
|
b921ca8c9d | ||
|
29b38632e2 | ||
|
56193f833f | ||
|
2c8b977001 | ||
|
4827fcf243 | ||
|
9140ed6969 | ||
|
24e7597c67 | ||
|
833dd654b2 | ||
|
ae74154071 | ||
|
fb1631c1c7 | ||
|
1c6832b9b2 | ||
|
77d06037bb | ||
|
136e62b97d | ||
|
24c36087dd | ||
|
73f9d721fe | ||
|
792f8b2d1f | ||
|
6871d194a8 | ||
|
12c26e4551 | ||
|
3426f650fa | ||
|
f224dc241e | ||
|
f6effb3c40 | ||
|
6bbdcc060d | ||
|
14411d8c86 | ||
|
cca2bfee4e | ||
|
c6cc68601b | ||
|
06f33e4bdc | ||
|
616b38158a | ||
|
c22f88ae42 | ||
|
3bf401f029 | ||
|
0b8b74b7a4 | ||
|
e1bd0ee3d7 | ||
|
4b0d95ed0c | ||
|
fedb146025 | ||
|
695a5eb470 | ||
|
f6e4d909ff | ||
|
6c0299b05a | ||
|
fb02095568 | ||
|
d5675ce498 | ||
|
ccbb860de1 | ||
|
5e104a3dd2 | ||
|
51890c94cc | ||
|
06259a2d63 | ||
|
d04ac09e82 | ||
|
cae9f03892 | ||
|
bffcd6107c | ||
|
056e0c816b | ||
|
ea67312877 | ||
|
327cdc8604 | ||
|
6f37cc4d01 | ||
|
003dd49ed6 | ||
|
46d57f02e7 | ||
|
30915a93e5 | ||
|
c64480dc33 | ||
|
4a9b1aff96 | ||
|
542e94ec9c | ||
|
9341558f61 | ||
|
6ea13b2c0d | ||
|
e57512f4fe | ||
|
348f67f4a4 | ||
|
83bdf07600 | ||
|
dfe4eddbbc | ||
|
1caed59f76 | ||
|
6db4e88a2c | ||
|
133daeb664 | ||
|
a4a8d1ecf0 | ||
|
88a08a74f7 | ||
|
c9e12a3027 | ||
|
82aa4a65ab | ||
|
d46237f1bf | ||
|
1744f94910 | ||
|
e308812429 | ||
|
2328ef0b0c | ||
|
000c5770f2 | ||
|
9e1a3598ab | ||
|
7268f60343 | ||
|
c8b274031f | ||
|
10394dbb1c | ||
|
859bb91fc7 | ||
|
0fd0d33704 | ||
|
ad0f489834 | ||
|
b1fa870de7 | ||
|
7c5e2ae8ea | ||
|
c0e4065a45 | ||
|
35448eea9f | ||
|
430f45a3ec | ||
|
71b6fd5326 | ||
|
251db733b2 | ||
|
d799725b8a | ||
|
9d80aed468 | ||
|
96f92f0908 | ||
|
314e4301f3 | ||
|
b284bb60f5 | ||
|
f99396ec50 | ||
|
886cf6ed1f | ||
|
74dd6e97a2 | ||
|
46fde2f1aa | ||
|
e2a3c360ea | ||
|
9968617758 | ||
|
1cec27ed70 | ||
|
f0dfff0625 | ||
|
fdaec3c959 | ||
|
fcb4c6a972 | ||
|
513974bbed | ||
|
00d6effd69 | ||
|
c06c68d5da |
@ -8,3 +8,6 @@ celerybeat.pid
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
.history/
|
||||
.idea
|
||||
.venv/
|
||||
.env
|
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,4 +0,0 @@
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||
|
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "09:30"
|
||||
timezone: "Asia/Shanghai"
|
||||
target-branch: dev
|
74
.github/workflows/build-base-image.yml
vendored
Normal file
74
.github/workflows/build-base-image.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
name: Build and Push Base Image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'v*'
|
||||
paths:
|
||||
- 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-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract date
|
||||
id: vars
|
||||
run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV
|
||||
|
||||
- name: Extract repository name
|
||||
id: repo
|
||||
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push multi-arch image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: Dockerfile-base
|
||||
tags: jumpserver/core-base:${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Update Dockerfile
|
||||
run: |
|
||||
sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
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 origin ${{ github.event.pull_request.head.ref }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
31
.github/workflows/check-compilemessages.yml
vendored
Normal file
31
.github/workflows/check-compilemessages.yml
vendored
Normal 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
|
24
.github/workflows/discord-release.yml
vendored
Normal file
24
.github/workflows/discord-release.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Publish Release to Discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
send_discord_notification:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.release.tag_name, 'v4.')
|
||||
steps:
|
||||
- name: Send release notification to Discord
|
||||
env:
|
||||
WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }}
|
||||
run: |
|
||||
# 获取标签名称和 release body
|
||||
TAG_NAME="${{ github.event.release.tag_name }}"
|
||||
RELEASE_BODY="${{ github.event.release.body }}"
|
||||
|
||||
# 使用 jq 构建 JSON 数据,以确保安全传递
|
||||
JSON_PAYLOAD=$(jq -n --arg tag "# JumpServer $TAG_NAME Released! 🚀" --arg body "$RELEASE_BODY" '{content: "\($tag)\n\($body)"}')
|
||||
|
||||
# 使用 curl 发送 JSON 数据
|
||||
curl -X POST -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"
|
24
.github/workflows/docs-release.yml
vendored
Normal file
24
.github/workflows/docs-release.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Auto update docs changelog
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
update_docs_changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.release.tag_name, 'v4.')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Update docs changelog
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}
|
||||
run: |
|
||||
git config --global user.name 'BaiJiangJie'
|
||||
git config --global user.email 'jiangjie.bai@fit2cloud.com'
|
||||
|
||||
git clone https://$DOCS_TOKEN@github.com/jumpservice/documentation.git
|
||||
cd documentation/utils
|
||||
bash update_changelog.sh
|
55
.github/workflows/jms-build-test.yml
vendored
55
.github/workflows/jms-build-test.yml
vendored
@ -1,55 +0,0 @@
|
||||
name: "Run Build Test"
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile-*'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check Dockerfile
|
||||
run: |
|
||||
test -f Dockerfile-ce || cp -f Dockerfile Dockerfile-ce
|
||||
|
||||
- name: Build CE Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: Dockerfile-ce
|
||||
tags: jumpserver/core-ce:test
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
PIP_JMS_MIRROR=https://pypi.org/simple
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Prepare EE Image
|
||||
run: |
|
||||
sed -i 's@^FROM registry.fit2cloud.com@# FROM registry.fit2cloud.com@g' Dockerfile-ee
|
||||
sed -i 's@^COPY --from=build-xpack@# COPY --from=build-xpack@g' Dockerfile-ee
|
||||
|
||||
- name: Build EE Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: Dockerfile-ee
|
||||
tags: jumpserver/core-ee:test
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
PIP_JMS_MIRROR=https://pypi.org/simple
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
63
.github/workflows/jms-build-test.yml.disabled
vendored
Normal file
63
.github/workflows/jms-build-test.yml.disabled
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
name: "Run Build Test"
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile*'
|
||||
- 'Dockerfile-*'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
component: [core]
|
||||
version: [v4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Prepare Build
|
||||
run: |
|
||||
sed -i 's@^FROM registry.fit2cloud.com/jumpserver@FROM ghcr.io/jumpserver@g' Dockerfile-ee
|
||||
|
||||
- name: Build CE Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: Dockerfile
|
||||
tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}-ce
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
VERSION=${{ matrix.version }}
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build EE Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: Dockerfile-ee
|
||||
tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
VERSION=${{ matrix.version }}
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
28
.github/workflows/llm-code-review.yml.bak
vendored
Normal file
28
.github/workflows/llm-code-review.yml.bak
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: LLM Code Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
llm-code-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: fit2cloud/LLM-CodeReview-Action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
|
||||
LANGUAGE: English
|
||||
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
MODEL: qwen2-1.5b-instruct
|
||||
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
|
||||
top_p: 1
|
||||
temperature: 1
|
||||
# max_tokens: 10000
|
||||
MAX_PATCH_LENGTH: 10000
|
||||
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
|
||||
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
|
45
.github/workflows/translate-readme.yml
vendored
Normal file
45
.github/workflows/translate-readme.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Translate README
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_readme:
|
||||
description: "Source README"
|
||||
required: false
|
||||
default: "./readmes/README.en.md"
|
||||
target_langs:
|
||||
description: "Target Languages"
|
||||
required: false
|
||||
default: "zh-hans,zh-hant,ja,pt-br,es,ru"
|
||||
gen_dir_path:
|
||||
description: "Generate Dir Name"
|
||||
required: false
|
||||
default: "readmes/"
|
||||
push_branch:
|
||||
description: "Push Branch"
|
||||
required: false
|
||||
default: "pr@dev@translate_readme"
|
||||
prompt:
|
||||
description: "AI Translate Prompt"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
gpt_mode:
|
||||
description: "GPT Mode"
|
||||
required: false
|
||||
default: "gpt-4o-mini"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Auto Translate
|
||||
uses: jumpserver-dev/action-translate-readme@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }}
|
||||
GPT_MODE: ${{ github.event.inputs.gpt_mode }}
|
||||
SOURCE_README: ${{ github.event.inputs.source_readme }}
|
||||
TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }}
|
||||
PUSH_BRANCH: ${{ github.event.inputs.push_branch }}
|
||||
GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }}
|
||||
PROMPT: ${{ github.event.inputs.prompt }}
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -44,3 +44,8 @@ data/*
|
||||
test.py
|
||||
.history/
|
||||
.test/
|
||||
*.mo
|
||||
apps.iml
|
||||
*.db
|
||||
*.mmdb
|
||||
*.ipdb
|
||||
|
@ -1,3 +1,4 @@
|
||||
[settings]
|
||||
line_length=120
|
||||
known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets
|
||||
|
||||
|
2
.pylintrc
Normal file
2
.pylintrc
Normal file
@ -0,0 +1,2 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,too-many-ancestors
|
@ -1,5 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
As a contributor, you should agree that:
|
||||
|
||||
- The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary.
|
||||
- Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations.
|
||||
|
||||
## Create pull request
|
||||
PR are always welcome, even if they only contain small fixes like typos or a few lines of code. If there will be a significant effort, please document it as an issue and get a discussion going before starting to work on it.
|
||||
|
||||
|
68
Dockerfile
Normal file
68
Dockerfile
Normal file
@ -0,0 +1,68 @@
|
||||
FROM jumpserver/core-base:20250427_062456 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ADD . .
|
||||
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& \
|
||||
if [ -n "${VERSION}" ]; then \
|
||||
sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \
|
||||
fi
|
||||
|
||||
RUN set -ex \
|
||||
&& export SECRET_KEY=$(head -c100 < /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 48) \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& cd apps \
|
||||
&& python manage.py compilemessages
|
||||
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
libldap2-dev \
|
||||
libx11-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
cron \
|
||||
ca-certificates \
|
||||
default-libmysqlclient-dev \
|
||||
openssh-client \
|
||||
sshpass \
|
||||
bubblewrap"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
|
||||
RUN set -ex \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update > /dev/null \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& apt-get clean all \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "0 3 * * * root find /tmp -type f -mtime +1 -size +1M -exec rm -f {} \; && date > /tmp/clean.log" > /etc/cron.d/cleanup_tmp \
|
||||
&& chmod 0644 /etc/cron.d/cleanup_tmp
|
||||
|
||||
COPY --from=stage-build /opt /opt
|
||||
COPY --from=stage-build /usr/local/bin /usr/local/bin
|
||||
COPY --from=stage-build /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["start", "all"]
|
61
Dockerfile-base
Normal file
61
Dockerfile-base
Normal file
@ -0,0 +1,61 @@
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
|
||||
# Install APT dependencies
|
||||
ARG DEPENDENCIES=" \
|
||||
ca-certificates \
|
||||
wget \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
gettext \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update > /dev/null \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
# Install bin tools
|
||||
ARG CHECK_VERSION=v1.0.4
|
||||
RUN set -ex \
|
||||
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& mv check /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/check \
|
||||
&& chmod 755 /usr/local/bin/check \
|
||||
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
|
||||
|
||||
# Install Python dependencies
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
|
||||
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
--mount=type=bind,source=requirements/clean_site_packages.sh,target=clean_site_packages.sh \
|
||||
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
|
||||
--mount=type=bind,source=requirements/static_files.sh,target=utils/static_files.sh \
|
||||
set -ex \
|
||||
&& uv venv \
|
||||
&& uv pip install -i${PIP_MIRROR} -r pyproject.toml \
|
||||
&& ln -sf $(pwd)/.venv /opt/py3 \
|
||||
&& bash utils/static_files.sh \
|
||||
&& bash clean_site_packages.sh
|
137
Dockerfile-ce
137
Dockerfile-ce
@ -1,137 +0,0 @@
|
||||
FROM python:3.11-slim-bullseye as stage-1
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.11-slim-bullseye as stage-2
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
freerdp2-dev \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
git \
|
||||
git-lfs \
|
||||
unzip \
|
||||
xz-utils \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
ENV LANG=zh_CN.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
libjpeg-dev \
|
||||
libpq-dev \
|
||||
libx11-dev \
|
||||
freerdp2-dev \
|
||||
libxmlsec1-openssl"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
netcat-openbsd \
|
||||
nmap \
|
||||
openssh-client \
|
||||
patch \
|
||||
sshpass \
|
||||
telnet \
|
||||
vim \
|
||||
bubblewrap \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||
|
||||
ARG RECEPTOR_VERSION=v1.4.5
|
||||
RUN set -ex \
|
||||
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
|
||||
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/receptor \
|
||||
&& chmod 755 /usr/local/bin/receptor \
|
||||
&& rm -f /opt/receptor.tar.gz
|
||||
|
||||
COPY --from=stage-2 /opt/py3 /opt/py3
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
@ -1,5 +1,30 @@
|
||||
ARG VERSION
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||
FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
|
||||
ARG VERSION=dev
|
||||
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
|
||||
FROM jumpserver/core:${VERSION}-ce
|
||||
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
|
||||
ARG TOOLS=" \
|
||||
g++ \
|
||||
curl \
|
||||
iputils-ping \
|
||||
netcat-openbsd \
|
||||
nmap \
|
||||
telnet \
|
||||
vim \
|
||||
wget"
|
||||
|
||||
RUN set -ex \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& apt-get clean all \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
|
||||
RUN set -ex \
|
||||
&& uv pip install -i${PIP_MIRROR} --group xpack
|
||||
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
199
README.md
199
README.md
@ -1,125 +1,118 @@
|
||||
<p align="center">
|
||||
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
||||
</p>
|
||||
<h3 align="center">广受欢迎的开源堡垒机</h3>
|
||||
<div align="center">
|
||||
<a name="readme-top"></a>
|
||||
<a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
||||
|
||||
## An open-source PAM tool (Bastion Host)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Docker pulls"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver/releases/latest"><img src="https://img.shields.io/github/v/release/jumpserver/jumpserver" alt="Latest release"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
[![][license-shield]][license-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
[![][docker-shield]][docker-link]
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
|
||||
[English](/README.md) · [中文(简体)](/readmes/README.zh-hans.md) · [中文(繁體)](/readmes/README.zh-hant.md) · [日本語](/readmes/README.ja.md) · [Português (Brasil)](/readmes/README.pt-br.md) · [Español](/readmes/README.es.md) · [Русский](/readmes/README.ru.md)
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
## What is JumpServer?
|
||||
|
||||
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
|
||||
|
||||
|
||||
<p align="center">
|
||||
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||
</p>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/28676212-2bc4-4a9f-ae10-3be9320647e3">
|
||||
<img src="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f" alt="Theme-based Image">
|
||||
</picture>
|
||||
|
||||
------------------------------
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
|
||||
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||
## Quickstart
|
||||
|
||||
- **SSH**: Linux / Unix / 网络设备 等;
|
||||
- **Windows**: Web 方式连接 / 原生 RDP 连接;
|
||||
- **数据库**: MySQL / MariaDB / PostgreSQL / Oracle / SQLServer / ClickHouse 等;
|
||||
- **NoSQL**: Redis / MongoDB 等;
|
||||
- **GPT**: ChatGPT 等;
|
||||
- **云服务**: Kubernetes / VMware vSphere 等;
|
||||
- **Web 站点**: 各类系统的 Web 管理后台;
|
||||
- **应用**: 通过 Remote App 连接各类应用。
|
||||
Prepare a clean Linux Server ( 64 bit, >= 4c8g )
|
||||
|
||||
## 产品特色
|
||||
```sh
|
||||
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash
|
||||
```
|
||||
|
||||
- **开源**: 零门槛,线上快速获取和安装;
|
||||
- **无插件**: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- **分布式**: 支持分布式部署和横向扩展,轻松支持大规模并发访问;
|
||||
- **多云支持**: 一套系统,同时管理不同云上面的资产;
|
||||
- **多租户**: 一套系统,多个子公司或部门同时使用;
|
||||
- **云端存储**: 审计录像云端存储,永不丢失;
|
||||
Access JumpServer in your browser at `http://your-jumpserver-ip/`
|
||||
- Username: `admin`
|
||||
- Password: `ChangeMe`
|
||||
|
||||
## UI 展示
|
||||
[](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
|
||||
|
||||

|
||||
## Screenshots
|
||||
<table style="border-collapse: collapse; border: 1px solid black;">
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/99fabe5b-0475-4a53-9116-4c370a1426c4" alt="JumpServer Console" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/7c1f81af-37e8-4f07-8ac9-182895e1062e" alt="JumpServer PAM" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/a424d731-1c70-4108-a7d8-5bbf387dda9a" alt="JumpServer Audits" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/393d2c27-a2d0-4dea-882d-00ed509e00c9" alt="JumpServer Workbench" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/eaa41f66-8cc8-4f01-a001-0d258501f1c9" alt="JumpServer RBAC" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/3a2611cd-8902-49b8-b82b-2a6dac851f3e" alt="JumpServer Settings" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/1e236093-31f7-4563-8eb1-e36d865f1568" alt="JumpServer SSH" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/69373a82-f7ab-41e8-b763-bbad2ba52167" alt="JumpServer RDP" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/5bed98c6-cbe8-4073-9597-d53c69dc3957" alt="JumpServer K8s" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/b80ad654-548f-42bc-ba3d-c1cfdf1b46d6" alt="JumpServer DB" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 在线体验
|
||||
## Components
|
||||
|
||||
- 环境地址:<https://demo.jumpserver.org/>
|
||||
JumpServer consists of multiple key components, which collectively form the functional framework of JumpServer, providing users with comprehensive capabilities for operations management and security control.
|
||||
|
||||
| :warning: 注意 |
|
||||
|:-----------------------------|
|
||||
| 该环境仅作体验目的使用,我们会定时清理、重置数据! |
|
||||
| 请勿修改体验环境用户的密码! |
|
||||
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
|
||||
| Project | Status | Description |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector |
|
||||
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
|
||||
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Remote Application Connector (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
|
||||
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
|
||||
| [Nec](https://github.com/jumpserver/nec) | <img alt="Nec" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE VNC Proxy Connector |
|
||||
| [Facelive](https://github.com/jumpserver/facelive) | <img alt="Facelive" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Facial Recognition |
|
||||
|
||||
## 快速开始
|
||||
|
||||
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
|
||||
- [产品文档](https://docs.jumpserver.org)
|
||||
- [在线学习](https://edu.fit2cloud.com/page/2635362)
|
||||
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
|
||||
## Contributing
|
||||
|
||||
## 案例研究
|
||||
Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines.
|
||||
|
||||
- [腾讯音乐娱乐集团:基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
|
||||
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
|
||||
- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
|
||||
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
|
||||
- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
|
||||
- [携程:JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [大智慧:JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [小红书:JumpServer 堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [中手游:JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
|
||||
## License
|
||||
|
||||
## 社区交流
|
||||
Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved.
|
||||
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
|
||||
|
||||
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
|
||||
|
||||
### 参与贡献
|
||||
|
||||
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
|
||||
|
||||
## 组件项目
|
||||
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目 |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer RDP 代理 Connector 项目 |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Linux) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
|
||||
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
|
||||
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core API 通信的组件项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
## 安全说明
|
||||
|
||||
JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/)
|
||||
进行安装部署。如果您发现安全相关问题,请直接联系我们:
|
||||
|
||||
- 邮箱:support@fit2cloud.com
|
||||
- 电话:400-052-0755
|
||||
|
||||
## License & Copyright
|
||||
|
||||
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in
|
||||
compliance with the License. You may obtain a copy of the License at
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "
|
||||
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
|
||||
language governing permissions and limitations under the License.
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
<!-- JumpServer official link -->
|
||||
[docs-link]: https://jumpserver.com/docs
|
||||
[discord-link]: https://discord.com/invite/W6vYXmAQG2
|
||||
[contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
|
||||
|
||||
<!-- JumpServer Other link-->
|
||||
[license-link]: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
[docker-link]: https://hub.docker.com/u/jumpserver
|
||||
[github-release-link]: https://github.com/jumpserver/jumpserver/releases/latest
|
||||
[github-stars-link]: https://github.com/jumpserver/jumpserver
|
||||
[github-issues-link]: https://github.com/jumpserver/jumpserver/issues
|
||||
|
||||
<!-- Shield link-->
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/jumpserver/jumpserver
|
||||
[github-stars-shield]: https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square
|
||||
[docker-shield]: https://img.shields.io/docker/pulls/jumpserver/jms_all.svg
|
||||
[license-shield]: https://img.shields.io/github/license/jumpserver/jumpserver
|
||||
[discord-shield]: https://img.shields.io/discord/1194233267294052363?style=flat&logo=discord&logoColor=%23f5f5f5&labelColor=%235462eb&color=%235462eb
|
||||
|
94
README_EN.md
94
README_EN.md
@ -1,94 +0,0 @@
|
||||
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
|
||||
<h3 align="center">Open Source Bastion Host</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
|
||||
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
JumpServer is the world's first open-source Bastion Host and is licensed under the GPLv3. It is a 4A-compliant professional operation and maintenance security audit system.
|
||||
|
||||
JumpServer uses Python / Django for development, follows Web 2.0 specifications, and is equipped with an industry-leading Web Terminal solution that provides a beautiful user interface and great user experience
|
||||
|
||||
JumpServer adopts a distributed architecture to support multi-branch deployment across multiple cross-regional areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
|
||||
|
||||
Change the world by taking every little step
|
||||
|
||||
----
|
||||
### Advantages
|
||||
|
||||
- Open Source: huge transparency and free to access with quick installation process.
|
||||
- Distributed: support large-scale concurrent access with ease.
|
||||
- No Plugin required: all you need is a browser, the ultimate Web Terminal experience.
|
||||
- Multi-Cloud supported: a unified system to manage assets on different clouds at the same time
|
||||
- Cloud storage: audit records are stored in the cloud. Data lost no more!
|
||||
- Multi-Tenant system: multiple subsidiary companies or departments access the same system simultaneously.
|
||||
- Many applications supported: link to databases, windows remote applications, and Kubernetes cluster, etc.
|
||||
|
||||
|
||||
### JumpServer Component Projects
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer Character protocaol Connector, replace original Python Version [Coco](https://github.com/jumpserver/coco)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer Graphics protocol Connector,rely on [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
### Contribution
|
||||
If you have any good ideas or helping us to fix bugs, please submit a Pull Request and accept our thanks :)
|
||||
|
||||
Thanks to the following contributors for making JumpServer better everyday!
|
||||
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/koko/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/lina/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/luna/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
|
||||
</a>
|
||||
|
||||
### Thanks to
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web page connection RDP, SSH, VNC protocol equipment. JumpServer graphical connection dependent.
|
||||
- [OmniDB](https://omnidb.org/) Web page connection to databases. JumpServer Web database dependent.
|
||||
|
||||
|
||||
### JumpServer Enterprise Version
|
||||
- [Apply for it](https://jinshuju.net/f/kyOYpi)
|
||||
|
||||
### Case Study
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851);
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516);
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732);
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708);
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
|
||||
### For safety instructions
|
||||
|
||||
JumpServer is a security product. Please refer to [Basic Security Recommendations](https://docs.jumpserver.org/zh/master/install/install_security/) for deployment and installation.
|
||||
|
||||
If you find a security problem, please contact us directly:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2024 FIT2CLOUD Tech, Inc., All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
https://www.gnu.org/licenses/gpl-3.0.htmll
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
@ -5,8 +5,7 @@ JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议
|
||||
如果你发现安全问题,请直接联系我们,我们携手让世界更好:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
- support@lxware.hk
|
||||
|
||||
|
||||
# Security Policy
|
||||
@ -16,6 +15,5 @@ JumpServer is a security product, The installation and development should follow
|
||||
All security bugs should be reported to the contact as below:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
- support@lxware.hk
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
from .account import *
|
||||
from .application import *
|
||||
from .pam_dashboard import *
|
||||
from .task import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
|
@ -1,20 +1,27 @@
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.filters import AccountFilterSet
|
||||
from accounts.const import ChangeSecretRecordStatusChoice
|
||||
from accounts.filters import AccountFilterSet, NodeFilterBackend
|
||||
from accounts.mixins import AccountRecordViewLogMixin
|
||||
from accounts.models import Account
|
||||
from accounts.models import Account, ChangeSecretRecord
|
||||
from assets.models import Asset, Node
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.api.mixin import ExtraFilterFieldsMixin
|
||||
from common.drf.filters import AttrRulesFilterBackend
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = [
|
||||
'AccountViewSet', 'AccountSecretsViewSet',
|
||||
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
||||
@ -24,6 +31,7 @@ __all__ = [
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = Account
|
||||
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
||||
extra_filter_backends = [AttrRulesFilterBackend, NodeFilterBackend]
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
@ -33,9 +41,21 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
'partial_update': ['accounts.change_account'],
|
||||
'su_from_accounts': 'accounts.view_account',
|
||||
'clear_secret': 'accounts.change_account',
|
||||
'move_to_assets': 'accounts.create_account',
|
||||
'copy_to_assets': 'accounts.create_account',
|
||||
}
|
||||
export_as_zip = True
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
asset_id = self.request.query_params.get('asset') or self.request.query_params.get('asset_id')
|
||||
if not asset_id:
|
||||
return queryset
|
||||
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
queryset = asset.all_accounts.all()
|
||||
return queryset
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-accounts')
|
||||
def su_from_accounts(self, request, *args, **kwargs):
|
||||
account_id = request.query_params.get('account')
|
||||
@ -86,6 +106,45 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
||||
return Response(status=HTTP_200_OK)
|
||||
|
||||
def _copy_or_move_to_assets(self, request, move=False):
|
||||
account = self.get_object()
|
||||
asset_ids = request.data.get('assets', [])
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
field_names = [
|
||||
'name', 'username', 'secret_type', 'secret',
|
||||
'privileged', 'is_active', 'source', 'source_id', 'comment'
|
||||
]
|
||||
account_data = {field: getattr(account, field) for field in field_names}
|
||||
|
||||
creation_results = {}
|
||||
success_count = 0
|
||||
|
||||
for asset in assets:
|
||||
account_data['asset'] = asset
|
||||
creation_results[asset] = {'state': 'created'}
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.model.objects.create(**account_data)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.debug(f'{"Move" if move else "Copy"} to assets error: {e}')
|
||||
creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'}
|
||||
|
||||
results = [{'asset': str(asset), **res} for asset, res in creation_results.items()]
|
||||
|
||||
if move and success_count > 0:
|
||||
account.delete()
|
||||
|
||||
return Response(results, status=HTTP_200_OK)
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='move-to-assets')
|
||||
def move_to_assets(self, request, *args, **kwargs):
|
||||
return self._copy_or_move_to_assets(request, move=True)
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='copy-to-assets')
|
||||
def copy_to_assets(self, request, *args, **kwargs):
|
||||
return self._copy_or_move_to_assets(request, move=False)
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
@ -125,17 +184,31 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||
'GET': 'accounts.view_accountsecret',
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
@lazyproperty
|
||||
def account(self) -> Account:
|
||||
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
|
||||
|
||||
def get_object(self):
|
||||
return self.account
|
||||
|
||||
@lazyproperty
|
||||
def latest_history(self):
|
||||
return self.account.history.first()
|
||||
|
||||
@property
|
||||
def latest_change_secret_record(self) -> ChangeSecretRecord:
|
||||
return self.account.changesecretrecords.filter(
|
||||
status=ChangeSecretRecordStatusChoice.pending
|
||||
).order_by('-date_created').first()
|
||||
|
||||
@staticmethod
|
||||
def filter_spm_queryset(resource_ids, queryset):
|
||||
return queryset.filter(history_id__in=resource_ids)
|
||||
|
||||
def get_queryset(self):
|
||||
account = self.get_object()
|
||||
account = self.account
|
||||
histories = account.history.all()
|
||||
latest_history = account.history.first()
|
||||
latest_history = self.latest_history
|
||||
if not latest_history:
|
||||
return histories
|
||||
if account.secret != latest_history.secret:
|
||||
@ -144,3 +217,25 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||
return histories
|
||||
histories = histories.exclude(history_id=latest_history.history_id)
|
||||
return histories
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = list(queryset)
|
||||
latest_history = self.latest_history
|
||||
if not latest_history:
|
||||
return queryset
|
||||
|
||||
latest_change_secret_record = self.latest_change_secret_record
|
||||
if not latest_change_secret_record:
|
||||
return queryset
|
||||
|
||||
if latest_change_secret_record.date_created > latest_history.history_date:
|
||||
temp_history = self.model(
|
||||
secret=latest_change_secret_record.new_secret,
|
||||
secret_type=self.account.secret_type,
|
||||
version=latest_history.version,
|
||||
history_date=latest_change_secret_record.date_created,
|
||||
)
|
||||
queryset = [temp_history] + queryset
|
||||
|
||||
return queryset
|
||||
|
84
apps/accounts/api/account/application.py
Normal file
84
apps/accounts/api/account/application.py
Normal file
@ -0,0 +1,84 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _, get_language
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.models import IntegrationApplication
|
||||
from audits.models import IntegrationApplicationLog
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.exceptions import JMSException
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import get_request_ip
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
|
||||
class IntegrationApplicationViewSet(OrgBulkModelViewSet):
|
||||
model = IntegrationApplication
|
||||
search_fields = ('name', 'comment')
|
||||
serializer_classes = {
|
||||
'default': serializers.IntegrationApplicationSerializer,
|
||||
'get_account_secret': serializers.IntegrationAccountSecretSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
'get_once_secret': 'accounts.change_integrationapplication',
|
||||
'get_account_secret': 'accounts.view_integrationapplication'
|
||||
}
|
||||
|
||||
def read_file(self, path):
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding='utf-8') as file:
|
||||
return file.read()
|
||||
return ''
|
||||
|
||||
@action(
|
||||
['GET'], detail=False, url_path='sdks',
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def get_sdks_info(self, request, *args, **kwargs):
|
||||
code_suffix_mapper = {
|
||||
'python': 'py',
|
||||
'java': 'java',
|
||||
'go': 'go',
|
||||
'node': 'js',
|
||||
'curl': 'sh',
|
||||
}
|
||||
sdk_language = request.query_params.get('language', 'python')
|
||||
sdk_path = os.path.join(settings.APPS_DIR, 'accounts', 'demos', sdk_language)
|
||||
readme_path = os.path.join(sdk_path, f'README.{get_language()}.md')
|
||||
demo_path = os.path.join(sdk_path, f'demo.{code_suffix_mapper[sdk_language]}')
|
||||
|
||||
readme_content = self.read_file(readme_path)
|
||||
demo_content = self.read_file(demo_path)
|
||||
|
||||
return Response(data={'readme': readme_content, 'code': demo_content})
|
||||
|
||||
@action(
|
||||
['GET'], detail=True, url_path='secret',
|
||||
permission_classes=[RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||
)
|
||||
def get_once_secret(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
return Response(data={'id': instance.id, 'secret': instance.secret})
|
||||
|
||||
@action(['GET'], detail=False, url_path='account-secret',
|
||||
permission_classes=[RBACPermission])
|
||||
def get_account_secret(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.query_params)
|
||||
if not serializer.is_valid():
|
||||
return Response({'error': serializer.errors}, status=400)
|
||||
|
||||
service = request.user
|
||||
account = service.get_account(**serializer.data)
|
||||
if not account:
|
||||
msg = _('Account not found')
|
||||
raise JMSException(code='Not found', detail='%s' % msg)
|
||||
asset = account.asset
|
||||
IntegrationApplicationLog.objects.create(
|
||||
remote_addr=get_request_ip(request), service=service.name, service_id=service.id,
|
||||
account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})',
|
||||
)
|
||||
return Response(data={'id': request.user.id, 'secret': account.secret})
|
130
apps/accounts/api/account/pam_dashboard.py
Normal file
130
apps/accounts/api/account/pam_dashboard.py
Normal file
@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models import Count, F, Q
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from accounts.models import (
|
||||
Account, GatherAccountsAutomation,
|
||||
PushAccountAutomation, BackupAccountAutomation,
|
||||
AccountRisk, IntegrationApplication, ChangeSecretAutomation
|
||||
)
|
||||
from assets.const import AllTypes
|
||||
from common.utils.timezone import local_monday
|
||||
|
||||
__all__ = ['PamDashboardApi']
|
||||
|
||||
|
||||
class PamDashboardApi(APIView):
|
||||
http_method_names = ['get']
|
||||
rbac_perms = {
|
||||
'GET': 'accounts.view_account',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_type_to_accounts():
|
||||
result = Account.objects.annotate(type=F('asset__platform__type')) \
|
||||
.values('type').order_by('type').annotate(total=Count(1))
|
||||
all_types_dict = dict(AllTypes.choices())
|
||||
|
||||
return [
|
||||
{**i, 'label': all_types_dict.get(i['type'], i['type'])}
|
||||
for i in result
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_account_risk_data(_all, query_params):
|
||||
agg_map = {
|
||||
'total_long_time_no_login_accounts': ('long_time_no_login_count', Q(risk='long_time_no_login')),
|
||||
'total_new_found_accounts': ('new_found_count', Q(risk='new_found')),
|
||||
'total_groups_changed_accounts': ('groups_changed_count', Q(risk='groups_changed')),
|
||||
'total_sudoers_changed_accounts': ('sudoers_changed_count', Q(risk='sudoers_changed')),
|
||||
'total_authorized_keys_changed_accounts': (
|
||||
'authorized_keys_changed_count', Q(risk='authorized_keys_changed')),
|
||||
'total_account_deleted_accounts': ('account_deleted_count', Q(risk='account_deleted')),
|
||||
'total_password_expired_accounts': ('password_expired_count', Q(risk='password_expired')),
|
||||
'total_long_time_password_accounts': ('long_time_password_count', Q(risk='long_time_password')),
|
||||
'total_weak_password_accounts': ('weak_password_count', Q(risk='weak_password')),
|
||||
'total_leaked_password_accounts': ('leaked_password_count', Q(risk='leaked_password')),
|
||||
'total_repeated_password_accounts': ('repeated_password_count', Q(risk='repeated_password')),
|
||||
}
|
||||
|
||||
aggregations = {
|
||||
agg_key: Count('id', distinct=True, filter=agg_filter)
|
||||
for param_key, (agg_key, agg_filter) in agg_map.items()
|
||||
if _all or query_params.get(param_key)
|
||||
}
|
||||
|
||||
data = {}
|
||||
if aggregations:
|
||||
account_stats = AccountRisk.objects.aggregate(**aggregations)
|
||||
data = {param_key: account_stats.get(agg_key) for param_key, (agg_key, _) in agg_map.items() if
|
||||
agg_key in account_stats}
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_account_data(_all, query_params):
|
||||
agg_map = {
|
||||
'total_accounts': ('total_count', Count('id')),
|
||||
'total_privileged_accounts': ('privileged_count', Count('id', filter=Q(privileged=True))),
|
||||
'total_connectivity_ok_accounts': ('connectivity_ok_count', Count('id', filter=Q(connectivity='ok'))),
|
||||
'total_secret_reset_accounts': ('secret_reset_count', Count('id', filter=Q(secret_reset=True))),
|
||||
'total_valid_accounts': ('valid_count', Count('id', filter=Q(is_active=True))),
|
||||
'total_week_add_accounts': ('week_add_count', Count('id', filter=Q(date_created__gte=local_monday()))),
|
||||
}
|
||||
|
||||
aggregations = {
|
||||
agg_key: agg_expr
|
||||
for param_key, (agg_key, agg_expr) in agg_map.items()
|
||||
if _all or query_params.get(param_key)
|
||||
}
|
||||
|
||||
data = {}
|
||||
account_stats = Account.objects.aggregate(**aggregations)
|
||||
for param_key, (agg_key, __) in agg_map.items():
|
||||
if agg_key in account_stats:
|
||||
data[param_key] = account_stats[agg_key]
|
||||
|
||||
if _all or query_params.get('total_ordinary_accounts'):
|
||||
if 'total_count' in account_stats and 'privileged_count' in account_stats:
|
||||
data['total_ordinary_accounts'] = \
|
||||
account_stats['total_count'] - account_stats['privileged_count']
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_automation_counts(_all, query_params):
|
||||
automation_counts = defaultdict(int)
|
||||
automation_models = {
|
||||
'total_count_change_secret_automation': ChangeSecretAutomation,
|
||||
'total_count_gathered_account_automation': GatherAccountsAutomation,
|
||||
'total_count_push_account_automation': PushAccountAutomation,
|
||||
'total_count_backup_account_automation': BackupAccountAutomation,
|
||||
'total_count_integration_application': IntegrationApplication,
|
||||
}
|
||||
|
||||
for param_key, model in automation_models.items():
|
||||
if _all or query_params.get(param_key):
|
||||
automation_counts[param_key] = model.objects.count()
|
||||
|
||||
return automation_counts
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
query_params = self.request.query_params
|
||||
|
||||
_all = query_params.get('all')
|
||||
|
||||
data = {}
|
||||
data.update(self.get_account_data(_all, query_params))
|
||||
data.update(self.get_account_risk_data(_all, query_params))
|
||||
data.update(self.get_automation_counts(_all, query_params))
|
||||
|
||||
if _all or query_params.get('total_count_type_to_accounts'):
|
||||
data.update({
|
||||
'total_count_type_to_accounts': self.get_type_to_accounts(),
|
||||
})
|
||||
|
||||
return JsonResponse(data, status=200)
|
@ -1,5 +1,7 @@
|
||||
from .backup import *
|
||||
from .base import *
|
||||
from .change_secret import *
|
||||
from .gather_accounts import *
|
||||
from .change_secret_dashboard import *
|
||||
from .check_account import *
|
||||
from .gather_account import *
|
||||
from .push_account import *
|
||||
|
@ -1,41 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import (
|
||||
AccountBackupAutomation, AccountBackupExecution
|
||||
BackupAccountAutomation
|
||||
)
|
||||
from accounts.tasks import execute_account_backup_task
|
||||
from common.const.choices import Trigger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
__all__ = [
|
||||
'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet'
|
||||
'BackupAccountViewSet', 'BackupAccountExecutionViewSet'
|
||||
]
|
||||
|
||||
|
||||
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
||||
model = AccountBackupAutomation
|
||||
class BackupAccountViewSet(OrgBulkModelViewSet):
|
||||
model = BackupAccountAutomation
|
||||
filterset_fields = ('name',)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AccountBackupSerializer
|
||||
serializer_class = serializers.BackupAccountSerializer
|
||||
|
||||
|
||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||
search_fields = ('trigger', 'plan__name')
|
||||
filterset_fields = ('trigger', 'plan_id', 'plan__name')
|
||||
http_method_names = ['get', 'post', 'options']
|
||||
class BackupAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_backupaccountexecution"),
|
||||
("retrieve", "accounts.view_backupaccountexecution"),
|
||||
("create", "accounts.add_backupaccountexecution"),
|
||||
("report", "accounts.view_backupaccountexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.backup_account
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = AccountBackupExecution.objects.all()
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
pid = serializer.data.get('plan')
|
||||
task = execute_account_backup_task.delay(pid=str(pid), trigger=Trigger.manual)
|
||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||
|
@ -1,8 +1,12 @@
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from rest_framework import status, mixins, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts.filters import AutomationExecutionFilterSet
|
||||
from accounts.models import AutomationExecution
|
||||
from accounts.tasks import execute_account_automation_task
|
||||
from assets import serializers
|
||||
@ -13,7 +17,7 @@ from orgs.mixins import generics
|
||||
__all__ = [
|
||||
'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
|
||||
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi',
|
||||
'AutomationExecutionViewSet',
|
||||
'AutomationExecutionViewSet'
|
||||
]
|
||||
|
||||
|
||||
@ -35,9 +39,10 @@ class AutomationAssetsListApi(generics.ListAPIView):
|
||||
return assets
|
||||
|
||||
|
||||
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
|
||||
class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||
model = BaseAutomation
|
||||
serializer_class = serializers.UpdateAssetSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@ -52,9 +57,10 @@ class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
|
||||
return Response({'msg': 'ok'})
|
||||
|
||||
|
||||
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
|
||||
class AutomationAddAssetApi(generics.UpdateAPIView):
|
||||
model = BaseAutomation
|
||||
serializer_class = serializers.UpdateAssetSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@ -68,9 +74,10 @@ class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
|
||||
return Response({"error": serializer.errors})
|
||||
|
||||
|
||||
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
|
||||
class AutomationNodeAddRemoveApi(generics.UpdateAPIView):
|
||||
model = BaseAutomation
|
||||
serializer_class = serializers.UpdateNodeSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
action_params = ['add', 'remove']
|
||||
@ -97,8 +104,8 @@ class AutomationExecutionViewSet(
|
||||
):
|
||||
search_fields = ('trigger', 'automation__name')
|
||||
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||
filterset_class = AutomationExecutionFilterSet
|
||||
serializer_class = serializers.AutomationExecutionSerializer
|
||||
|
||||
tp: str
|
||||
|
||||
def get_queryset(self):
|
||||
@ -113,3 +120,10 @@ class AutomationExecutionViewSet(
|
||||
pid=str(automation.pk), trigger=Trigger.manual, tp=self.tp
|
||||
)
|
||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@action(methods=['get'], detail=True, url_path='report')
|
||||
def report(self, request, *args, **kwargs):
|
||||
execution = self.get_object()
|
||||
report = execution.manager.gen_report()
|
||||
return HttpResponse(report)
|
||||
|
@ -1,15 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Max, Q, Subquery, OuterRef
|
||||
from rest_framework import status, mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
|
||||
from accounts.filters import ChangeSecretRecordFilterSet
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
||||
from accounts.tasks import execute_automation_record_task
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.permissions import IsValidLicense
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from .base import (
|
||||
@ -27,6 +29,7 @@ __all__ = [
|
||||
|
||||
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = ChangeSecretAutomation
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
filterset_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.ChangeSecretAutomationSerializer
|
||||
@ -34,7 +37,9 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||
|
||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
filterset_class = ChangeSecretRecordFilterSet
|
||||
search_fields = ('asset__address',)
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
search_fields = ('asset__address', 'account__username')
|
||||
ordering_fields = ('date_finished',)
|
||||
tp = AutomationTypes.change_secret
|
||||
serializer_classes = {
|
||||
'default': serializers.ChangeSecretRecordSerializer,
|
||||
@ -43,6 +48,8 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
rbac_perms = {
|
||||
'execute': 'accounts.add_changesecretexecution',
|
||||
'secret': 'accounts.view_changesecretrecord',
|
||||
'dashboard': 'accounts.view_changesecretrecord',
|
||||
'ignore_fail': 'accounts.view_changesecretrecord',
|
||||
}
|
||||
|
||||
def get_permissions(self):
|
||||
@ -53,8 +60,35 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
]
|
||||
return super().get_permissions()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
if self.action == 'dashboard':
|
||||
return self.get_dashboard_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def get_dashboard_queryset(queryset):
|
||||
recent_dates = queryset.values('account').annotate(
|
||||
max_date_finished=Max('date_finished')
|
||||
)
|
||||
|
||||
recent_success_accounts = queryset.filter(
|
||||
account=OuterRef('account'),
|
||||
date_finished=Subquery(
|
||||
recent_dates.filter(account=OuterRef('account')).values('max_date_finished')[:1]
|
||||
)
|
||||
).filter(Q(status=ChangeSecretRecordStatusChoice.success))
|
||||
|
||||
failed_records = queryset.filter(
|
||||
~Q(account__in=Subquery(recent_success_accounts.values('account'))),
|
||||
status=ChangeSecretRecordStatusChoice.failed,
|
||||
ignore_fail=False
|
||||
)
|
||||
return failed_records
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.all()
|
||||
return ChangeSecretRecord.get_valid_records()
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='execute')
|
||||
def execute(self, request, *args, **kwargs):
|
||||
@ -75,19 +109,31 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='dashboard')
|
||||
def dashboard(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@action(methods=['patch'], detail=True, url_path='ignore-fail')
|
||||
def ignore_fail(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.ignore_fail = True
|
||||
instance.save(update_fields=['ignore_fail'])
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_changesecretexecution"),
|
||||
("retrieve", "accounts.view_changesecretexecution"),
|
||||
("create", "accounts.add_changesecretexecution"),
|
||||
("report", "accounts.view_changesecretexecution"),
|
||||
)
|
||||
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
tp = AutomationTypes.change_secret
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(automation__type=self.tp)
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
|
||||
|
185
apps/accounts/api/automations/change_secret_dashboard.py
Normal file
185
apps/accounts/api/automations/change_secret_dashboard.py
Normal file
@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http.response import JsonResponse
|
||||
from django.utils import timezone
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
|
||||
from accounts.models import ChangeSecretAutomation, AutomationExecution, ChangeSecretRecord
|
||||
from assets.models import Node, Asset
|
||||
from common.const import Status
|
||||
from common.permissions import IsValidLicense
|
||||
from common.utils import lazyproperty
|
||||
from common.utils.timezone import local_zero_hour, local_now
|
||||
from ops.celery import app
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
__all__ = ['ChangeSecretDashboardApi']
|
||||
|
||||
|
||||
class ChangeSecretDashboardApi(APIView):
|
||||
http_method_names = ['get']
|
||||
rbac_perms = {
|
||||
'GET': 'accounts.view_changesecretautomation',
|
||||
}
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
tp = AutomationTypes.change_secret
|
||||
task_name = 'accounts.tasks.automation.execute_account_automation_task'
|
||||
ongoing_change_secret_cache_key = "ongoing_change_secret_cache_key"
|
||||
|
||||
@lazyproperty
|
||||
def days(self):
|
||||
count = self.request.query_params.get('days', 1)
|
||||
return int(count)
|
||||
|
||||
@property
|
||||
def days_to_datetime(self):
|
||||
if self.days == 1:
|
||||
return local_zero_hour()
|
||||
return local_now() - timezone.timedelta(days=self.days)
|
||||
|
||||
def get_queryset_date_filter(self, qs, query_field='date_updated'):
|
||||
return qs.filter(**{f'{query_field}__gte': self.days_to_datetime})
|
||||
|
||||
@lazyproperty
|
||||
def date_range_list(self):
|
||||
return [
|
||||
(local_now() - timezone.timedelta(days=i)).date()
|
||||
for i in range(self.days - 1, -1, -1)
|
||||
]
|
||||
|
||||
def filter_by_date_range(self, queryset, field_name):
|
||||
date_range_bounds = self.days_to_datetime.date(), (local_now() + timezone.timedelta(days=1)).date()
|
||||
return queryset.filter(**{f'{field_name}__range': date_range_bounds})
|
||||
|
||||
def calculate_daily_metrics(self, queryset, date_field):
|
||||
filtered_queryset = self.filter_by_date_range(queryset, date_field)
|
||||
results = filtered_queryset.values_list(date_field, 'status')
|
||||
|
||||
status_counts = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
for date_finished, status in results:
|
||||
date_str = str(date_finished.date())
|
||||
if status == ChangeSecretRecordStatusChoice.failed:
|
||||
status_counts[date_str]['failed'] += 1
|
||||
elif status == ChangeSecretRecordStatusChoice.success:
|
||||
status_counts[date_str]['success'] += 1
|
||||
|
||||
metrics = defaultdict(list)
|
||||
for date in self.date_range_list:
|
||||
date_str = str(date)
|
||||
for status in ['success', 'failed']:
|
||||
metrics[status].append(status_counts[date_str].get(status, 0))
|
||||
|
||||
return metrics
|
||||
|
||||
def get_daily_success_and_failure_metrics(self):
|
||||
metrics = self.calculate_daily_metrics(self.change_secret_records_queryset, 'date_finished')
|
||||
return metrics.get('success', []), metrics.get('failed', [])
|
||||
|
||||
@lazyproperty
|
||||
def change_secrets_queryset(self):
|
||||
return ChangeSecretAutomation.objects.all()
|
||||
|
||||
@lazyproperty
|
||||
def change_secret_records_queryset(self):
|
||||
return ChangeSecretRecord.get_valid_records()
|
||||
|
||||
def get_change_secret_asset_queryset(self):
|
||||
qs = self.change_secrets_queryset
|
||||
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
|
||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||
return Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
def get_filtered_counts(self, qs, field=None):
|
||||
if field is None:
|
||||
return qs.count()
|
||||
return self.get_queryset_date_filter(qs, field).count()
|
||||
|
||||
def get_status_counts(self, executions):
|
||||
executions = executions.filter(type=self.tp)
|
||||
total, failed, success = 0, 0, 0
|
||||
for status in executions.values_list('status', flat=True):
|
||||
total += 1
|
||||
if status in [Status.failed, Status.error]:
|
||||
failed += 1
|
||||
elif status == Status.success:
|
||||
success += 1
|
||||
|
||||
return {
|
||||
'total_count_change_secret_executions': total,
|
||||
'total_count_success_change_secret_executions': success,
|
||||
'total_count_failed_change_secret_executions': failed,
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
query_params = self.request.query_params
|
||||
data = {}
|
||||
|
||||
_all = query_params.get('all')
|
||||
|
||||
if _all or query_params.get('total_count_change_secrets'):
|
||||
data['total_count_change_secrets'] = self.get_filtered_counts(
|
||||
self.change_secrets_queryset
|
||||
)
|
||||
|
||||
if _all or query_params.get('total_count_periodic_change_secrets'):
|
||||
data['total_count_periodic_change_secrets'] = self.get_filtered_counts(
|
||||
self.change_secrets_queryset.filter(is_periodic=True)
|
||||
)
|
||||
|
||||
if _all or query_params.get('total_count_change_secret_assets'):
|
||||
data['total_count_change_secret_assets'] = self.get_change_secret_asset_queryset().count()
|
||||
|
||||
if _all or query_params.get('total_count_change_secret_status'):
|
||||
executions = self.get_queryset_date_filter(AutomationExecution.objects.all(), 'date_start')
|
||||
data.update(self.get_status_counts(executions))
|
||||
|
||||
if _all or query_params.get('daily_success_and_failure_metrics'):
|
||||
success, failed = self.get_daily_success_and_failure_metrics()
|
||||
data.update({
|
||||
'dates_metrics_date': [date.strftime('%m-%d') for date in self.date_range_list] or ['0'],
|
||||
'dates_metrics_total_count_success': success,
|
||||
'dates_metrics_total_count_failed': failed,
|
||||
})
|
||||
|
||||
if _all or query_params.get('total_count_ongoing_change_secret'):
|
||||
ongoing_counts = cache.get(self.ongoing_change_secret_cache_key)
|
||||
if ongoing_counts is None:
|
||||
execution_ids = []
|
||||
inspect = app.control.inspect()
|
||||
try:
|
||||
active_tasks = inspect.active()
|
||||
except Exception:
|
||||
active_tasks = None
|
||||
if active_tasks:
|
||||
for tasks in active_tasks.values():
|
||||
for task in tasks:
|
||||
_id = task.get('id')
|
||||
name = task.get('name')
|
||||
tp = task.get('kwargs', {}).get('tp')
|
||||
if name == self.task_name and tp == self.tp:
|
||||
execution_ids.append(_id)
|
||||
|
||||
snapshots = AutomationExecution.objects.filter(id__in=execution_ids).values_list('snapshot', flat=True)
|
||||
|
||||
asset_ids = {asset for i in snapshots for asset in i.get('assets', [])}
|
||||
account_ids = {account for i in snapshots for account in i.get('accounts', [])}
|
||||
|
||||
ongoing_counts = (len(execution_ids), len(asset_ids), len(account_ids))
|
||||
data['total_count_ongoing_change_secret'] = ongoing_counts[0]
|
||||
data['total_count_ongoing_change_secret_assets'] = ongoing_counts[1]
|
||||
data['total_count_ongoing_change_secret_accounts'] = ongoing_counts[2]
|
||||
cache.set(self.ongoing_change_secret_cache_key, ongoing_counts, 60)
|
||||
else:
|
||||
data['total_count_ongoing_change_secret'] = ongoing_counts[0]
|
||||
data['total_count_ongoing_change_secret_assets'] = ongoing_counts[1]
|
||||
data['total_count_ongoing_change_secret_accounts'] = ongoing_counts[2]
|
||||
|
||||
return JsonResponse(data, status=200)
|
162
apps/accounts/api/automations/check_account.py
Normal file
162
apps/accounts/api/automations/check_account.py
Normal file
@ -0,0 +1,162 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Q, Count
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import (
|
||||
CheckAccountAutomation,
|
||||
AccountRisk,
|
||||
RiskChoice,
|
||||
CheckAccountEngine,
|
||||
AutomationExecution,
|
||||
)
|
||||
from assets.models import Asset
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidLicense
|
||||
from common.utils import many_get
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from .base import AutomationExecutionViewSet
|
||||
from ...filters import NodeFilterBackend
|
||||
from ...risk_handlers import RiskHandler
|
||||
|
||||
__all__ = [
|
||||
"CheckAccountAutomationViewSet",
|
||||
"CheckAccountExecutionViewSet",
|
||||
"AccountRiskViewSet",
|
||||
"CheckAccountEngineViewSet",
|
||||
]
|
||||
|
||||
|
||||
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = CheckAccountAutomation
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
serializer_class = serializers.CheckAccountAutomationSerializer
|
||||
|
||||
|
||||
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_checkaccountexecution"),
|
||||
("retrieve", "accounts.view_checkaccountsexecution"),
|
||||
("create", "accounts.add_checkaccountexecution"),
|
||||
("adhoc", "accounts.add_checkaccountexecution"),
|
||||
("report", "accounts.view_checkaccountsexecution"),
|
||||
)
|
||||
ordering = ("-date_created",)
|
||||
tp = AutomationTypes.check_account
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
@action(methods=["get"], detail=False, url_path="adhoc")
|
||||
def adhoc(self, request, *args, **kwargs):
|
||||
asset_id = request.query_params.get("asset_id")
|
||||
if not asset_id:
|
||||
return Response(status=400, data={"asset_id": "This field is required."})
|
||||
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
name = "Check asset risk: {}".format(asset.name)
|
||||
execution = AutomationExecution()
|
||||
execution.snapshot = {
|
||||
"assets": [asset_id],
|
||||
"nodes": [],
|
||||
"type": AutomationTypes.check_account,
|
||||
"engines": "__all__",
|
||||
"name": name,
|
||||
}
|
||||
execution.save()
|
||||
execution.start()
|
||||
report = execution.manager.gen_report()
|
||||
return HttpResponse(report)
|
||||
|
||||
|
||||
class AccountRiskViewSet(OrgBulkModelViewSet):
|
||||
model = AccountRisk
|
||||
search_fields = ["username", "asset__name"]
|
||||
filterset_fields = ("risk", "status", "asset_id")
|
||||
extra_filter_backends = [NodeFilterBackend]
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
serializer_classes = {
|
||||
"default": serializers.AccountRiskSerializer,
|
||||
"assets": serializers.AssetRiskSerializer,
|
||||
"handle": serializers.HandleRiskSerializer,
|
||||
}
|
||||
ordering_fields = ("asset", "risk", "status", "username", "date_created")
|
||||
ordering = ("status", "asset", "date_created")
|
||||
rbac_perms = {
|
||||
"sync_accounts": "assets.add_accountrisk",
|
||||
"assets": "accounts.view_accountrisk",
|
||||
"handle": "accounts.change_accountrisk",
|
||||
}
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed("PUT")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed("POST")
|
||||
|
||||
@action(methods=["get"], detail=False, url_path="assets")
|
||||
def assets(self, request, *args, **kwargs):
|
||||
annotations = {
|
||||
f"{risk[0]}_count": Count("id", filter=Q(risk=risk[0]))
|
||||
for risk in RiskChoice.choices
|
||||
}
|
||||
queryset = (
|
||||
AccountRisk.objects.select_related(
|
||||
"asset", "asset__platform"
|
||||
) # 使用 select_related 来优化 asset 和 asset__platform 的查询
|
||||
.values(
|
||||
"asset__id", "asset__name", "asset__address", "asset__platform__name"
|
||||
) # 添加需要的字段
|
||||
.annotate(risk_total=Count("id")) # 计算风险总数
|
||||
.annotate(**annotations) # 使用上面定义的 annotations 进行计数
|
||||
)
|
||||
return self.get_paginated_response_from_queryset(queryset)
|
||||
|
||||
@action(methods=["post"], detail=False, url_path="handle")
|
||||
def handle(self, request, *args, **kwargs):
|
||||
s = self.get_serializer(data=request.data)
|
||||
s.is_valid(raise_exception=True)
|
||||
|
||||
asset, username, act, risk = many_get(
|
||||
s.validated_data, ("asset", "username", "action", "risk")
|
||||
)
|
||||
handler = RiskHandler(asset=asset, username=username, request=self.request)
|
||||
|
||||
try:
|
||||
risk = handler.handle(act, risk)
|
||||
s = serializers.AccountRiskSerializer(instance=risk)
|
||||
return Response(data=s.data)
|
||||
except Exception as e:
|
||||
return Response(status=400, data=str(e))
|
||||
|
||||
|
||||
class CheckAccountEngineViewSet(JMSModelViewSet):
|
||||
search_fields = ("name",)
|
||||
serializer_class = serializers.CheckAccountEngineSerializer
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
perm_model = CheckAccountEngine
|
||||
http_method_names = ['get', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
return CheckAccountEngine.get_default_engines()
|
||||
|
||||
def filter_queryset(self, queryset: list):
|
||||
search = self.request.GET.get('search')
|
||||
if search is not None:
|
||||
queryset = [
|
||||
item for item in queryset
|
||||
if search in item['name']
|
||||
]
|
||||
return queryset
|
131
apps/accounts/api/automations/gather_account.py
Normal file
131
apps/accounts/api/automations/gather_account.py
Normal file
@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.filters import GatheredAccountFilterSet, NodeFilterBackend
|
||||
from accounts.models import GatherAccountsAutomation, AutomationExecution, Account
|
||||
from accounts.models import GatheredAccount
|
||||
from assets.models import Asset
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.utils.http import is_true
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
__all__ = [
|
||||
"DiscoverAccountsAutomationViewSet",
|
||||
"DiscoverAccountsExecutionViewSet",
|
||||
"GatheredAccountViewSet",
|
||||
]
|
||||
|
||||
from ...risk_handlers import RiskHandler
|
||||
|
||||
|
||||
class DiscoverAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = GatherAccountsAutomation
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.DiscoverAccountAutomationSerializer
|
||||
|
||||
|
||||
class DiscoverAccountsExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_gatheraccountsexecution"),
|
||||
("retrieve", "accounts.view_gatheraccountsexecution"),
|
||||
("create", "accounts.add_gatheraccountsexecution"),
|
||||
("adhoc", "accounts.add_gatheraccountsexecution"),
|
||||
("report", "accounts.view_gatheraccountsexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.gather_accounts
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@action(methods=["get"], detail=False, url_path="adhoc")
|
||||
def adhoc(self, request, *args, **kwargs):
|
||||
asset_id = request.query_params.get("asset_id")
|
||||
if not asset_id:
|
||||
return Response(status=400, data={"asset_id": "This field is required."})
|
||||
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
execution = AutomationExecution()
|
||||
execution.snapshot = {
|
||||
"assets": [asset_id],
|
||||
"nodes": [],
|
||||
"type": "gather_accounts",
|
||||
"is_sync_account": False,
|
||||
"check_risk": True,
|
||||
"name": "Adhoc gather accounts: {}".format(asset.name),
|
||||
}
|
||||
execution.save()
|
||||
execution.start()
|
||||
report = execution.manager.gen_report()
|
||||
return HttpResponse(report)
|
||||
|
||||
|
||||
class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||
model = GatheredAccount
|
||||
search_fields = ("username",)
|
||||
filterset_class = GatheredAccountFilterSet
|
||||
extra_filter_backends = [NodeFilterBackend]
|
||||
ordering = ("status",)
|
||||
serializer_classes = {
|
||||
"default": serializers.DiscoverAccountSerializer,
|
||||
"status": serializers.DiscoverAccountActionSerializer,
|
||||
"details": serializers.DiscoverAccountDetailsSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
"status": "assets.change_gatheredaccount",
|
||||
"details": "assets.view_gatheredaccount"
|
||||
}
|
||||
|
||||
@action(methods=["put"], detail=False, url_path="status")
|
||||
def status(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
validated_data = serializer.validated_data
|
||||
ids = validated_data.get('ids', [])
|
||||
new_status = validated_data.get('status')
|
||||
updated_instances = GatheredAccount.objects.filter(id__in=ids).select_related('asset')
|
||||
if new_status == ConfirmOrIgnore.confirmed:
|
||||
GatheredAccount.sync_accounts(updated_instances)
|
||||
updated_instances.update(present=True)
|
||||
updated_instances.update(status=new_status)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
request = self.request
|
||||
params = request.query_params
|
||||
is_delete_remote = params.get("is_delete_remote")
|
||||
is_delete_account = params.get("is_delete_account")
|
||||
asset_id = params.get("asset")
|
||||
username = params.get("username")
|
||||
if is_true(is_delete_remote):
|
||||
self._delete_remote(asset_id, username)
|
||||
if is_true(is_delete_account):
|
||||
account = get_object_or_404(Account, username=username, asset_id=asset_id)
|
||||
account.delete()
|
||||
super().perform_destroy(instance)
|
||||
|
||||
def _delete_remote(self, asset_id, username):
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
handler = RiskHandler(asset, username, request=self.request)
|
||||
handler.handle_delete_remote()
|
||||
|
||||
@action(methods=["get"], detail=True, url_path="details")
|
||||
def details(self, request, *args, **kwargs):
|
||||
pk = kwargs.get('pk')
|
||||
account = get_object_or_404(GatheredAccount, pk=pk)
|
||||
serializer = self.get_serializer(account.detail)
|
||||
return Response(data=serializer.data)
|
@ -1,59 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.filters import GatheredAccountFilterSet
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
from accounts.models import GatheredAccount
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
__all__ = [
|
||||
'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet',
|
||||
'GatheredAccountViewSet'
|
||||
]
|
||||
|
||||
|
||||
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = GatherAccountsAutomation
|
||||
filterset_fields = ('name',)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.GatherAccountAutomationSerializer
|
||||
|
||||
|
||||
class GatherAccountsExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_gatheraccountsexecution"),
|
||||
("retrieve", "accounts.view_gatheraccountsexecution"),
|
||||
("create", "accounts.add_gatheraccountsexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.gather_accounts
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(automation__type=self.tp)
|
||||
return queryset
|
||||
|
||||
|
||||
class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||
model = GatheredAccount
|
||||
search_fields = ('username',)
|
||||
filterset_class = GatheredAccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.GatheredAccountSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'sync_accounts': 'assets.add_gatheredaccount',
|
||||
}
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='sync-accounts')
|
||||
def sync_accounts(self, request, *args, **kwargs):
|
||||
gathered_account_ids = request.data.get('gathered_account_ids')
|
||||
gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids)
|
||||
self.model.sync_accounts(gathered_accounts)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
@ -1,15 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import mixins
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import PushAccountAutomation, ChangeSecretRecord
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from accounts.filters import PushAccountRecordFilterSet
|
||||
from accounts.models import PushAccountAutomation, PushSecretRecord
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from .base import (
|
||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
|
||||
)
|
||||
from .change_secret import ChangeSecretRecordViewSet
|
||||
|
||||
__all__ = [
|
||||
'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi',
|
||||
@ -30,24 +31,28 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
("list", "accounts.view_pushaccountexecution"),
|
||||
("retrieve", "accounts.view_pushaccountexecution"),
|
||||
("create", "accounts.add_pushaccountexecution"),
|
||||
("report", "accounts.view_pushaccountexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.push_account
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(automation__type=self.tp)
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
|
||||
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
class PushAccountRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
filterset_class = PushAccountRecordFilterSet
|
||||
search_fields = ('asset__address', 'account__username')
|
||||
ordering_fields = ('date_finished',)
|
||||
tp = AutomationTypes.push_account
|
||||
serializer_classes = {
|
||||
'default': serializers.PushSecretRecordSerializer,
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.filter(
|
||||
execution__automation__type=AutomationTypes.push_account
|
||||
)
|
||||
return PushSecretRecord.get_valid_records()
|
||||
|
||||
|
||||
class PushAccountAssetsListApi(AutomationAssetsListApi):
|
||||
|
@ -4,6 +4,7 @@ from django.apps import AppConfig
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'accounts'
|
||||
verbose_name = 'App Accounts'
|
||||
|
||||
def ready(self):
|
||||
from . import signal_handlers # noqa
|
||||
|
@ -3,20 +3,24 @@ import time
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AccountBackupType
|
||||
from accounts.models.automations.backup_account import AccountBackupAutomation
|
||||
from accounts.models import BackupAccountAutomation, Account
|
||||
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
||||
from accounts.serializers import AccountSecretSerializer
|
||||
from assets.const import AllTypes
|
||||
from common.const import Status
|
||||
from common.utils.file import encrypt_and_compress_zip_file, zip_files
|
||||
from common.utils.timezone import local_now_filename, local_now_display
|
||||
from terminal.models.component.storage import ReplayStorage
|
||||
from users.models import User
|
||||
|
||||
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||
split_help_text = _('The account key will be split into two parts and sent')
|
||||
|
||||
|
||||
class RecipientsNotFound(Exception):
|
||||
@ -32,17 +36,26 @@ class BaseAccountHandler:
|
||||
if isinstance(v, OrderedDict):
|
||||
cls.unpack_data(v, data)
|
||||
else:
|
||||
if isinstance(v, dict):
|
||||
v = v.get('label')
|
||||
elif v is None:
|
||||
v = ''
|
||||
data[k] = v
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def get_header_fields(cls, serializer: serializers.Serializer):
|
||||
try:
|
||||
backup_fields = getattr(serializer, 'Meta').fields_backup
|
||||
exclude_backup_fields = getattr(serializer, 'Meta').exclude_backup_fields
|
||||
except AttributeError:
|
||||
backup_fields = serializer.fields.keys()
|
||||
exclude_backup_fields = []
|
||||
backup_fields = serializer.fields.keys()
|
||||
|
||||
header_fields = {}
|
||||
for field in backup_fields:
|
||||
if field in exclude_backup_fields:
|
||||
continue
|
||||
|
||||
v = serializer.fields[field]
|
||||
if isinstance(v, serializers.Serializer):
|
||||
_fields = cls.get_header_fields(v)
|
||||
@ -72,9 +85,9 @@ class BaseAccountHandler:
|
||||
|
||||
class AssetAccountHandler(BaseAccountHandler):
|
||||
@staticmethod
|
||||
def get_filename(plan_name):
|
||||
def get_filename(name):
|
||||
filename = os.path.join(
|
||||
PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.xlsx'
|
||||
PATH, f'{name}-{local_now_filename()}-{time.time()}.xlsx'
|
||||
)
|
||||
return filename
|
||||
|
||||
@ -115,32 +128,42 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
data = AccountSecretSerializer(_accounts, many=True).data
|
||||
cls.handler_secret(data, section)
|
||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||
|
||||
print('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
|
||||
number_of_backup_accounts = _('Number of backup accounts')
|
||||
print('\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count()))
|
||||
return data_map
|
||||
|
||||
|
||||
class AccountBackupHandler:
|
||||
def __init__(self, execution):
|
||||
def __init__(self, manager, execution):
|
||||
self.manager = manager
|
||||
self.execution = execution
|
||||
self.plan_name = self.execution.plan.name
|
||||
self.is_frozen = False # 任务状态冻结标志
|
||||
self.name = self.execution.snapshot.get('name', '-')
|
||||
|
||||
def get_accounts(self):
|
||||
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
|
||||
types = self.execution.snapshot.get('types', [])
|
||||
self.manager.summary['total_types'] = len(types)
|
||||
qs = Account.objects.filter(
|
||||
asset__platform__type__in=types
|
||||
).annotate(type=F('asset__platform__type'))
|
||||
return qs
|
||||
|
||||
def create_excel(self, section='complete'):
|
||||
hint = _('Generating asset related backup information files')
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
||||
f'\033[32m>>> {hint}\033[0m'
|
||||
''
|
||||
)
|
||||
# Print task start date
|
||||
|
||||
time_start = time.time()
|
||||
files = []
|
||||
accounts = self.execution.backup_accounts
|
||||
accounts = self.get_accounts()
|
||||
self.manager.summary['total_accounts'] = accounts.count()
|
||||
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
||||
if not data_map:
|
||||
return files
|
||||
|
||||
filename = AssetAccountHandler.get_filename(self.plan_name)
|
||||
filename = AssetAccountHandler.get_filename(self.name)
|
||||
|
||||
wb = Workbook(filename)
|
||||
for sheet, data in data_map.items():
|
||||
@ -151,28 +174,30 @@ class AccountBackupHandler:
|
||||
wb.close()
|
||||
files.append(filename)
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
print('创建备份文件完成: 用时 {}s'.format(timedelta))
|
||||
time_cost = _('Duration')
|
||||
file_created = _('Backup file creation completed')
|
||||
print('{}: {} {}s'.format(file_created, time_cost, timedelta))
|
||||
return files
|
||||
|
||||
def send_backup_mail(self, files, recipients):
|
||||
if not files:
|
||||
return
|
||||
recipients = User.objects.filter(id__in=list(recipients))
|
||||
msg = _("Start sending backup emails")
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 开始发送备份邮件\033[0m'
|
||||
f'\033[32m>>> {msg}\033[0m'
|
||||
''
|
||||
)
|
||||
plan_name = self.plan_name
|
||||
name = self.name
|
||||
for user in recipients:
|
||||
if not user.secret_key:
|
||||
attachment_list = []
|
||||
else:
|
||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
||||
attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, user.secret_key, files)
|
||||
attachment_list = [attachment, ]
|
||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
||||
print('邮件已发送至{}({})'.format(user, user.email))
|
||||
attachment_list = [attachment]
|
||||
AccountBackupExecutionTaskMsg(name, user).publish(attachment_list)
|
||||
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
@ -181,73 +206,53 @@ class AccountBackupHandler:
|
||||
return
|
||||
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 开始发送备份文件到sftp服务器\033[0m'
|
||||
'\033[32m>>> 📃 ---> sftp \033[0m'
|
||||
''
|
||||
)
|
||||
plan_name = self.plan_name
|
||||
name = self.name
|
||||
encrypt_file = _('Encrypting files using encryption password')
|
||||
for rec in recipients:
|
||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
||||
attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
if password:
|
||||
print('\033[32m>>> 使用加密密码对文件进行加密中\033[0m')
|
||||
print(f'\033[32m>>> {encrypt_file}\033[0m')
|
||||
encrypt_and_compress_zip_file(attachment, password, files)
|
||||
else:
|
||||
zip_files(attachment, files)
|
||||
attachment_list = attachment
|
||||
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list)
|
||||
print('备份文件将发送至{}({})'.format(rec.name, rec.id))
|
||||
AccountBackupByObjStorageExecutionTaskMsg(name, rec).publish(attachment_list)
|
||||
file_sent_to = _('The backup file will be sent to')
|
||||
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
def step_perform_task_update(self, is_success, reason):
|
||||
self.execution.reason = reason[:1024]
|
||||
self.execution.is_success = is_success
|
||||
self.execution.save()
|
||||
print('\n已完成对任务状态的更新\n')
|
||||
|
||||
@staticmethod
|
||||
def step_finished(is_success):
|
||||
if is_success:
|
||||
print('任务执行成功')
|
||||
else:
|
||||
print('任务执行失败')
|
||||
|
||||
def _run(self):
|
||||
is_success = False
|
||||
error = '-'
|
||||
try:
|
||||
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value)
|
||||
if backup_type == AccountBackupType.email.value:
|
||||
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email)
|
||||
if backup_type == AccountBackupType.email:
|
||||
self.backup_by_email()
|
||||
elif backup_type == AccountBackupType.object_storage.value:
|
||||
elif backup_type == AccountBackupType.object_storage:
|
||||
self.backup_by_obj_storage()
|
||||
except Exception as e:
|
||||
self.is_frozen = True
|
||||
print('任务执行被异常中断')
|
||||
print('下面打印发生异常的 Traceback 信息 : ')
|
||||
print(e)
|
||||
error = str(e)
|
||||
else:
|
||||
is_success = True
|
||||
finally:
|
||||
reason = error
|
||||
self.step_perform_task_update(is_success, reason)
|
||||
self.step_finished(is_success)
|
||||
print(f'\033[31m>>> {error}\033[0m')
|
||||
self.execution.status = Status.error
|
||||
self.execution.summary['error'] = error
|
||||
|
||||
def backup_by_obj_storage(self):
|
||||
object_id = self.execution.snapshot.get('id')
|
||||
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password
|
||||
zip_encrypt_password = BackupAccountAutomation.objects.get(id=object_id).zip_encrypt_password
|
||||
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
|
||||
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
|
||||
no_assigned_sftp_server = _('The backup task has no assigned sftp server')
|
||||
if not obj_recipients_part_one and not obj_recipients_part_two:
|
||||
print(
|
||||
'\n'
|
||||
'\033[31m>>> 该备份任务未分配sftp服务器\033[0m'
|
||||
f'\033[31m>>> {no_assigned_sftp_server}\033[0m'
|
||||
''
|
||||
)
|
||||
raise RecipientsNotFound('Not Found Recipients')
|
||||
if obj_recipients_part_one and obj_recipients_part_two:
|
||||
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
|
||||
print(f'\033[32m>>> {split_help_text}\033[0m')
|
||||
files = self.create_excel(section='front')
|
||||
self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password)
|
||||
|
||||
@ -259,17 +264,18 @@ class AccountBackupHandler:
|
||||
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
|
||||
|
||||
def backup_by_email(self):
|
||||
warn_text = _('The backup task has no assigned recipient')
|
||||
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||
if not recipients_part_one and not recipients_part_two:
|
||||
print(
|
||||
'\n'
|
||||
'\033[31m>>> 该备份任务未分配收件人\033[0m'
|
||||
f'\033[31m>>> {warn_text}\033[0m'
|
||||
''
|
||||
)
|
||||
raise RecipientsNotFound('Not Found Recipients')
|
||||
return
|
||||
if recipients_part_one and recipients_part_two:
|
||||
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
|
||||
print(f'\033[32m>>> {split_help_text}\033[0m')
|
||||
files = self.create_excel(section='front')
|
||||
self.send_backup_mail(files, recipients_part_one)
|
||||
|
||||
@ -281,15 +287,5 @@ class AccountBackupHandler:
|
||||
self.send_backup_mail(files, recipients)
|
||||
|
||||
def run(self):
|
||||
print('任务开始: {}'.format(local_now_display()))
|
||||
time_start = time.time()
|
||||
try:
|
||||
self._run()
|
||||
except Exception as e:
|
||||
print('任务运行出现异常')
|
||||
print('下面显示异常 Traceback 信息: ')
|
||||
print(e)
|
||||
finally:
|
||||
print('\n任务结束: {}'.format(local_now_display()))
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
print('用时: {}s'.format(timedelta))
|
||||
print('{}: {}'.format(_('Plan start'), local_now_display()))
|
||||
self._run()
|
||||
|
@ -1,44 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.automations.base.manager import BaseManager
|
||||
from common.utils.timezone import local_now_display
|
||||
from .handlers import AccountBackupHandler
|
||||
|
||||
|
||||
class AccountBackupManager:
|
||||
def __init__(self, execution):
|
||||
self.execution = execution
|
||||
self.date_start = timezone.now()
|
||||
self.time_start = time.time()
|
||||
self.date_end = None
|
||||
self.time_end = None
|
||||
self.timedelta = 0
|
||||
|
||||
class AccountBackupManager(BaseManager):
|
||||
def do_run(self):
|
||||
execution = self.execution
|
||||
print('\n\033[33m# 账号备份计划正在执行\033[0m')
|
||||
handler = AccountBackupHandler(execution)
|
||||
account_backup_execution_being_executed = _('The account backup plan is being executed')
|
||||
print(f'\033[33m# {account_backup_execution_being_executed}\033[0m')
|
||||
handler = AccountBackupHandler(self, execution)
|
||||
handler.run()
|
||||
|
||||
def pre_run(self):
|
||||
self.execution.date_start = self.date_start
|
||||
self.execution.save()
|
||||
|
||||
def post_run(self):
|
||||
self.time_end = time.time()
|
||||
self.date_end = timezone.now()
|
||||
def send_report_if_need(self):
|
||||
pass
|
||||
|
||||
def print_summary(self):
|
||||
print('\n\n' + '-' * 80)
|
||||
print('计划执行结束 {}\n'.format(local_now_display()))
|
||||
self.timedelta = self.time_end - self.time_start
|
||||
print('用时: {}s'.format(self.timedelta))
|
||||
self.execution.timedelta = self.timedelta
|
||||
self.execution.save()
|
||||
plan_execution_end = _('Plan execution end')
|
||||
print('{} {}\n'.format(plan_execution_end, local_now_display()))
|
||||
time_cost = _('Duration')
|
||||
print('{}: {}s'.format(time_cost, self.duration))
|
||||
|
||||
def run(self):
|
||||
self.pre_run()
|
||||
self.do_run()
|
||||
self.post_run()
|
||||
def get_report_template(self):
|
||||
return "accounts/backup_account_report.html"
|
||||
|
@ -1,12 +1,203 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.automations.methods import platform_automation_methods
|
||||
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice
|
||||
from accounts.models import BaseAccountQuerySet
|
||||
from accounts.utils import SecretGenerator
|
||||
from assets.automations.base.manager import BasePlaybookManager
|
||||
from assets.const import HostTypes
|
||||
from common.db.utils import safe_db_connection
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AccountBasePlaybookManager(BasePlaybookManager):
|
||||
template_path = ''
|
||||
|
||||
@property
|
||||
def platform_automation_methods(self):
|
||||
return platform_automation_methods
|
||||
|
||||
|
||||
class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
)
|
||||
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
||||
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
|
||||
)
|
||||
self.account_ids = self.execution.snapshot['accounts']
|
||||
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
|
||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||
|
||||
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ssh_params(self, secret, secret_type):
|
||||
kwargs = {}
|
||||
if secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
kwargs['strategy'] = self.ssh_key_change_strategy
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
def get_secret(self, account):
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
new_secret = self.execution.snapshot['secret']
|
||||
else:
|
||||
generator = SecretGenerator(
|
||||
self.secret_strategy, self.secret_type,
|
||||
self.execution.snapshot.get('password_rules')
|
||||
)
|
||||
new_secret = generator.get_secret()
|
||||
return new_secret
|
||||
|
||||
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
||||
if not privilege_account:
|
||||
print('Not privilege account')
|
||||
return
|
||||
|
||||
asset = privilege_account.asset
|
||||
accounts = asset.all_accounts.all()
|
||||
accounts = accounts.filter(id__in=self.account_ids, secret_reset=True)
|
||||
|
||||
if self.secret_type:
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
|
||||
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
||||
accounts = accounts.filter(privileged=False).exclude(
|
||||
username__in=['root', 'administrator', privilege_account.username]
|
||||
)
|
||||
return accounts
|
||||
|
||||
def handle_ssh_secret(self, secret_type, new_secret, path_dir):
|
||||
private_key_path = None
|
||||
if secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
return new_secret, private_key_path
|
||||
|
||||
def gen_inventory(self, h, account, new_secret, private_key_path, asset):
|
||||
secret_type = account.secret_type
|
||||
h['ssh_params'].update(self.get_ssh_params(new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'full_username': account.full_username,
|
||||
'secret_type': secret_type,
|
||||
'secret': account.escape_jinja2_syntax(new_secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
return h
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
host = super().host_callback(
|
||||
host, asset=asset, account=account, automation=automation,
|
||||
path_dir=path_dir, **kwargs
|
||||
)
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
|
||||
host['ssh_params'] = {}
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
error_msg = _("No pending accounts found")
|
||||
if not accounts:
|
||||
print(f'{asset}: {error_msg}')
|
||||
return []
|
||||
|
||||
if asset.type == HostTypes.WINDOWS:
|
||||
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
||||
|
||||
inventory_hosts = []
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
||||
try:
|
||||
h = self.gen_account_inventory(account, asset, h, path_dir)
|
||||
except Exception as e:
|
||||
h['error'] = str(e)
|
||||
inventory_hosts.append(h)
|
||||
|
||||
return inventory_hosts
|
||||
|
||||
@staticmethod
|
||||
def save_record(recorder):
|
||||
recorder.save(update_fields=['error', 'status', 'date_finished'])
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
||||
recorder.date_finished = timezone.now()
|
||||
|
||||
account = recorder.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
|
||||
account.secret = getattr(recorder, 'new_secret', account.secret)
|
||||
account.date_updated = timezone.now()
|
||||
account.date_change_secret = timezone.now()
|
||||
account.change_secret_status = ChangeSecretRecordStatusChoice.success
|
||||
|
||||
self.summary['ok_accounts'] += 1
|
||||
self.result['ok_accounts'].append(
|
||||
{
|
||||
"asset": str(account.asset),
|
||||
"username": account.username,
|
||||
}
|
||||
)
|
||||
super().on_host_success(host, result)
|
||||
|
||||
with safe_db_connection():
|
||||
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
|
||||
self.save_record(recorder)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
||||
recorder.date_finished = timezone.now()
|
||||
recorder.error = error
|
||||
account = recorder.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.date_updated = timezone.now()
|
||||
account.date_change_secret = timezone.now()
|
||||
account.change_secret_status = ChangeSecretRecordStatusChoice.failed
|
||||
|
||||
self.summary['fail_accounts'] += 1
|
||||
self.result['fail_accounts'].append(
|
||||
{
|
||||
"asset": str(recorder.asset),
|
||||
"username": recorder.account.username,
|
||||
}
|
||||
)
|
||||
super().on_host_error(host, error, result)
|
||||
|
||||
with safe_db_connection():
|
||||
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
|
||||
self.save_record(recorder)
|
||||
|
@ -20,6 +20,7 @@
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||
register: ping_info
|
||||
delegate_to: localhost
|
||||
|
||||
@ -39,9 +40,12 @@
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
commands: "{{ params.commands }}"
|
||||
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
||||
answers: "{{ params.answers }}"
|
||||
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||
delay_time: "{{ params.delay_time | default(2) }}"
|
||||
prompt: "{{ params.prompt | default('.*') }}"
|
||||
ignore_errors: true
|
||||
when: ping_info is succeeded
|
||||
when: ping_info is succeeded and check_conn_after_change
|
||||
register: change_info
|
||||
delegate_to: localhost
|
||||
|
||||
@ -58,4 +62,6 @@
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||
delegate_to: localhost
|
||||
when: check_conn_after_change
|
@ -10,10 +10,30 @@ protocol: ssh
|
||||
priority: 50
|
||||
params:
|
||||
- name: commands
|
||||
type: list
|
||||
type: text
|
||||
label: "{{ 'Params commands label' | trans }}"
|
||||
default: [ '' ]
|
||||
default: ''
|
||||
help_text: "{{ 'Params commands help text' | trans }}"
|
||||
- name: recv_timeout
|
||||
type: int
|
||||
label: "{{ 'Params recv_timeout label' | trans }}"
|
||||
default: 30
|
||||
help_text: "{{ 'Params recv_timeout help text' | trans }}"
|
||||
- name: delay_time
|
||||
type: int
|
||||
label: "{{ 'Params delay_time label' | trans }}"
|
||||
default: 2
|
||||
help_text: "{{ 'Params delay_time help text' | trans }}"
|
||||
- name: prompt
|
||||
type: str
|
||||
label: "{{ 'Params prompt label' | trans }}"
|
||||
default: '.*'
|
||||
help_text: "{{ 'Params prompt help text' | trans }}"
|
||||
- name: answers
|
||||
type: text
|
||||
label: "{{ 'Params answer label' | trans }}"
|
||||
default: '.*'
|
||||
help_text: "{{ 'Params answer help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
SSH account change secret:
|
||||
@ -22,11 +42,91 @@ i18n:
|
||||
en: 'Custom password change by SSH command line'
|
||||
|
||||
Params commands help text:
|
||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
||||
zh: |
|
||||
请将命令中的指定位置改成特殊符号 <br />
|
||||
1. 改密账号 -> {username} <br />
|
||||
2. 改密密码 -> {password} <br />
|
||||
3. 登录用户密码 -> {login_password} <br />
|
||||
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<br />
|
||||
比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />
|
||||
enable <br />
|
||||
{login_password} <br />
|
||||
configure terminal <br />
|
||||
username {username} privilege 0 password {password} <br />
|
||||
end <br />
|
||||
ja: |
|
||||
コマンド内の指定された位置を特殊記号に変更してください。<br />
|
||||
新しいパスワード(アカウント変更) -> {username} <br />
|
||||
新しいパスワード(パスワード変更) -> {password} <br />
|
||||
ログインユーザーパスワード -> {login_password} <br />
|
||||
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
|
||||
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります:<br />
|
||||
enable <br />
|
||||
{login_password} <br />
|
||||
configure terminal <br />
|
||||
username {username} privilege 0 password {password} <br />
|
||||
end <br />
|
||||
en: |
|
||||
Please change the specified positions in the command to special symbols. <br />
|
||||
Change password account -> {username} <br />
|
||||
Change password -> {password} <br />
|
||||
Login user password -> {login_password} <br />
|
||||
<strong>Multiple commands are separated by new lines,</strong> and when executing tasks, <br />
|
||||
the system will replace the special symbols with real data. <br />
|
||||
For example, to change the password for a Cisco device, you generally need to configure five commands: <br />
|
||||
enable <br />
|
||||
{login_password} <br />
|
||||
configure terminal <br />
|
||||
username {username} privilege 0 password {password} <br />
|
||||
end <br />
|
||||
|
||||
Params commands label:
|
||||
zh: '自定义命令'
|
||||
ja: 'カスタムコマンド'
|
||||
en: 'Custom command'
|
||||
|
||||
Params recv_timeout label:
|
||||
zh: '超时时间'
|
||||
ja: 'タイムアウト'
|
||||
en: 'Timeout'
|
||||
|
||||
Params recv_timeout help text:
|
||||
zh: '等待命令结果返回的超时时间(秒)'
|
||||
ja: 'コマンドの結果を待つタイムアウト時間(秒)'
|
||||
en: 'The timeout for waiting for the command result to return (Seconds)'
|
||||
|
||||
Params delay_time label:
|
||||
zh: '延迟发送时间'
|
||||
ja: '遅延送信時間'
|
||||
en: 'Delayed send time'
|
||||
|
||||
Params delay_time help text:
|
||||
zh: '每条命令延迟发送的时间间隔(秒)'
|
||||
ja: '各コマンド送信の遅延間隔(秒)'
|
||||
en: 'Time interval for each command delay in sending (Seconds)'
|
||||
|
||||
Params prompt label:
|
||||
zh: '提示符'
|
||||
ja: 'ヒント'
|
||||
en: 'Prompt'
|
||||
|
||||
Params prompt help text:
|
||||
zh: '终端连接后显示的提示符信息(正则表达式)'
|
||||
ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)'
|
||||
en: 'Prompt information displayed after terminal connection (Regular expression)'
|
||||
|
||||
Params answer label:
|
||||
zh: '命令结果'
|
||||
ja: 'コマンド結果'
|
||||
en: 'Command result'
|
||||
|
||||
Params answer help text:
|
||||
zh: |
|
||||
根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式)
|
||||
ja: |
|
||||
結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。
|
||||
入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん)
|
||||
en: |
|
||||
Decide whether to execute the next command based on the result match.
|
||||
The input content corresponds line by line with the content
|
||||
of the `Custom command` above. (Regular expression)
|
||||
|
@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
@ -53,3 +53,4 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when: check_conn_after_change
|
@ -1,9 +1,12 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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,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 }}"
|
||||
filter: version
|
||||
when: check_conn_after_change
|
@ -1,7 +1,7 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
@ -40,3 +40,4 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
when: check_conn_after_change
|
||||
|
@ -1,7 +1,11 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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,8 @@
|
||||
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: check_conn_after_change
|
||||
|
@ -1,7 +1,7 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
@ -64,3 +64,4 @@
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT @@version
|
||||
when: check_conn_after_change
|
||||
|
@ -9,55 +9,20 @@
|
||||
database: passwd
|
||||
key: "{{ account.username }}"
|
||||
register: user_info
|
||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- 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: "{{ true if params.groups | length > 0 else false }}"
|
||||
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: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: user_info.msg is defined
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
@ -67,9 +32,61 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.msg is defined or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- 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: "{{ 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:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@ -79,14 +96,16 @@
|
||||
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('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@ -95,7 +114,9 @@
|
||||
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"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
@ -5,6 +5,12 @@ type:
|
||||
- AIX
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@ -28,12 +34,23 @@ 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)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
|
||||
en: 'Using Ansible module user to change account secret (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@ -49,6 +66,16 @@ 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 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@ -59,3 +86,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
@ -9,55 +9,20 @@
|
||||
database: passwd
|
||||
key: "{{ account.username }}"
|
||||
register: user_info
|
||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- 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: "{{ true if params.groups | length > 0 else false }}"
|
||||
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: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: user_info.msg is defined
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
@ -67,9 +32,61 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.msg is defined or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- 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: "{{ 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:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@ -79,14 +96,16 @@
|
||||
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('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@ -95,7 +114,9 @@
|
||||
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"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
@ -6,6 +6,12 @@ type:
|
||||
- linux
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@ -30,12 +36,23 @@ 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)'
|
||||
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
|
||||
en: 'Using Ansible module user to change account secret (SHA512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@ -51,6 +68,16 @@ 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 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@ -61,3 +88,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
@ -4,10 +4,6 @@
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
- name: Change password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
@ -28,4 +24,4 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
||||
|
@ -0,0 +1,27 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
- name: Change password
|
||||
community.windows.win_domain_user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
update_password: always
|
||||
password_never_expires: yes
|
||||
state: present
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.windows.win_ping:
|
||||
vars:
|
||||
ansible_user: "{{ account.full_username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
@ -0,0 +1,27 @@
|
||||
id: change_secret_ad_windows
|
||||
name: "{{ 'Windows account change secret' | trans }}"
|
||||
version: 1
|
||||
method: change_secret
|
||||
category:
|
||||
- ds
|
||||
type:
|
||||
- windows_ad
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret:
|
||||
zh: '使用 Ansible 模块 win_domain_user 执行 Windows 账号改密'
|
||||
ja: 'Ansible win_domain_user モジュールを使用して Windows アカウントのパスワード変更'
|
||||
en: 'Using Ansible module win_domain_user to change Windows account secret'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
@ -4,10 +4,6 @@
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
- name: Change password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
@ -25,11 +21,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 }}"
|
||||
when: account.secret_type == "password"
|
||||
gateway_args: "{{ jms_gateway | default({}) }}"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
||||
delegate_to: localhost
|
||||
|
@ -1,209 +1,57 @@
|
||||
import os
|
||||
import time
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||
from accounts.const import (
|
||||
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||
)
|
||||
from accounts.models import ChangeSecretRecord
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
|
||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from assets.const import HostTypes
|
||||
from common.utils import get_logger
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
from common.utils.timezone import local_now_filename
|
||||
from users.models import User
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
from ...utils import SecretGenerator
|
||||
from ..base.manager import BaseChangeSecretPushManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||
ansible_account_prefer = ''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.record_map = self.execution.snapshot.get('record_map', {})
|
||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
)
|
||||
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
||||
'ssh_key_change_strategy', SSHKeyStrategy.add
|
||||
)
|
||||
self.account_ids = self.execution.snapshot['accounts']
|
||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
def get_ssh_params(self, account, secret, secret_type):
|
||||
kwargs = {}
|
||||
if secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
kwargs['strategy'] = self.ssh_key_change_strategy
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||
record = self.get_or_create_record(asset, account, h['name'])
|
||||
new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir)
|
||||
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
|
||||
return h
|
||||
|
||||
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
|
||||
def get_or_create_record(self, asset, account, name):
|
||||
asset_account_id = f'{asset.id}-{account.id}'
|
||||
|
||||
def secret_generator(self, secret_type):
|
||||
return SecretGenerator(
|
||||
self.secret_strategy, secret_type,
|
||||
self.execution.snapshot.get('password_rules')
|
||||
)
|
||||
|
||||
def get_secret(self, secret_type):
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
return self.execution.snapshot['secret']
|
||||
if asset_account_id in self.record_map:
|
||||
record_id = self.record_map[asset_account_id]
|
||||
recorder = ChangeSecretRecord.objects.filter(id=record_id).first()
|
||||
else:
|
||||
return self.secret_generator(secret_type).get_secret()
|
||||
new_secret = self.get_secret(account)
|
||||
recorder = self.create_record(asset, account, new_secret)
|
||||
|
||||
def get_accounts(self, privilege_account):
|
||||
if not privilege_account:
|
||||
print(f'not privilege account')
|
||||
return []
|
||||
self.name_recorder_mapper[name] = recorder
|
||||
return recorder
|
||||
|
||||
asset = privilege_account.asset
|
||||
accounts = asset.accounts.all()
|
||||
accounts = accounts.filter(id__in=self.account_ids)
|
||||
if self.secret_type:
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
|
||||
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
||||
accounts = accounts.filter(privileged=False).exclude(
|
||||
username__in=['root', 'administrator', privilege_account.username]
|
||||
)
|
||||
return accounts
|
||||
|
||||
def host_callback(
|
||||
self, host, asset=None, account=None,
|
||||
automation=None, path_dir=None, **kwargs
|
||||
):
|
||||
host = super().host_callback(
|
||||
host, asset=asset, account=account, automation=automation,
|
||||
path_dir=path_dir, **kwargs
|
||||
def create_record(self, asset, account, new_secret):
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
comment=f'{account.username}@{asset.address}'
|
||||
)
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
if not accounts:
|
||||
print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % (
|
||||
asset.name, self.account_ids, self.secret_type
|
||||
))
|
||||
return []
|
||||
|
||||
records = []
|
||||
inventory_hosts = []
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
h['name'] += '(' + account.username + ')'
|
||||
if self.secret_type is None:
|
||||
new_secret = account.secret
|
||||
else:
|
||||
new_secret = self.get_secret(secret_type)
|
||||
|
||||
if new_secret is None:
|
||||
print(f'new_secret is None, account: {account}')
|
||||
continue
|
||||
|
||||
asset_account_id = f'{asset.id}-{account.id}'
|
||||
if asset_account_id not in self.record_map:
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
)
|
||||
records.append(recorder)
|
||||
else:
|
||||
record_id = self.record_map[asset_account_id]
|
||||
try:
|
||||
recorder = ChangeSecretRecord.objects.get(id=record_id)
|
||||
except ChangeSecretRecord.DoesNotExist:
|
||||
print(f"Record {record_id} not found")
|
||||
continue
|
||||
|
||||
self.name_recorder_mapper[h['name']] = recorder
|
||||
|
||||
private_key_path = None
|
||||
if secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret': account.escape_jinja2_syntax(new_secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
inventory_hosts.append(h)
|
||||
ChangeSecretRecord.objects.bulk_create(records)
|
||||
return inventory_hosts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
||||
recorder.date_finished = timezone.now()
|
||||
|
||||
account = recorder.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.secret = recorder.new_secret
|
||||
account.date_updated = timezone.now()
|
||||
|
||||
max_retries = 3
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
recorder.save()
|
||||
account.save(update_fields=['secret', 'version', 'date_updated'])
|
||||
break
|
||||
except Exception as e:
|
||||
retry_count += 1
|
||||
if retry_count == max_retries:
|
||||
self.on_host_error(host, str(e), result)
|
||||
else:
|
||||
print(f'retry {retry_count} times for {host} recorder save error: {e}')
|
||||
time.sleep(1)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
||||
recorder.date_finished = timezone.now()
|
||||
recorder.error = error
|
||||
try:
|
||||
recorder.save()
|
||||
except Exception as e:
|
||||
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
logger.error("Account error: ", e)
|
||||
return recorder
|
||||
|
||||
def check_secret(self):
|
||||
if self.secret_strategy == SecretStrategy.custom \
|
||||
@ -221,44 +69,39 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
else:
|
||||
failed += 1
|
||||
total += 1
|
||||
|
||||
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
||||
return summary
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
return
|
||||
super().run(*args, **kwargs)
|
||||
def print_summary(self):
|
||||
recorders = list(self.name_recorder_mapper.values())
|
||||
summary = self.get_summary(recorders)
|
||||
print(summary, end='')
|
||||
print('\n\n' + '-' * 80)
|
||||
plan_execution_end = _('Plan execution end')
|
||||
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
|
||||
time_cost = _('Duration')
|
||||
print('{}: {}s'.format(time_cost, self.duration))
|
||||
print(summary)
|
||||
|
||||
def send_report_if_need(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
return
|
||||
|
||||
recorders = list(self.name_recorder_mapper.values())
|
||||
if self.record_map:
|
||||
return
|
||||
|
||||
failed_recorders = [
|
||||
r for r in recorders
|
||||
if r.status == ChangeSecretRecordStatusChoice.failed.value
|
||||
]
|
||||
|
||||
recipients = self.execution.recipients
|
||||
recipients = User.objects.filter(id__in=list(recipients.keys()))
|
||||
if not recipients:
|
||||
return
|
||||
|
||||
if failed_recorders:
|
||||
name = self.execution.snapshot.get('name')
|
||||
execution_id = str(self.execution.id)
|
||||
_ids = [r.id for r in failed_recorders]
|
||||
asset_account_errors = ChangeSecretRecord.objects.filter(
|
||||
id__in=_ids).values_list('asset__name', 'account__username', 'error')
|
||||
|
||||
for user in recipients:
|
||||
ChangeSecretFailedMsg(name, execution_id, user, asset_account_errors).publish()
|
||||
context = self.get_report_context()
|
||||
for user in recipients:
|
||||
ChangeSecretReportMsg(user, context).publish()
|
||||
|
||||
if not recorders:
|
||||
return
|
||||
|
||||
summary = self.get_summary(recorders)
|
||||
self.send_recorder_mail(recipients, recorders, summary)
|
||||
|
||||
def send_recorder_mail(self, recipients, recorders, summary):
|
||||
@ -295,3 +138,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
return True
|
||||
|
||||
def get_report_template(self):
|
||||
return "accounts/change_secret_report.html"
|
||||
|
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
def is_weak_password(password):
|
||||
if len(password) < 8:
|
||||
return True
|
||||
|
||||
# 判断是否只有一种字符类型
|
||||
if password.isdigit() or password.isalpha():
|
||||
return True
|
||||
|
||||
# 判断是否只包含数字或字母
|
||||
if password.islower() or password.isupper():
|
||||
return True
|
||||
|
||||
# 判断是否包含常见弱密码
|
||||
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||
if password.lower() in common_passwords:
|
||||
return True
|
||||
|
||||
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||
if (
|
||||
not re.search(r"[A-Za-z]", password)
|
||||
or not re.search(r"[0-9]", password)
|
||||
or not re.search(r"[\W_]", password)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_it(fname):
|
||||
count = 0
|
||||
lines = []
|
||||
with open(fname, 'rb') as f:
|
||||
for line in f:
|
||||
try:
|
||||
line = line.decode().strip()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if len(line) > 32:
|
||||
continue
|
||||
|
||||
if is_weak_password(line):
|
||||
continue
|
||||
|
||||
lines.append(line)
|
||||
count += 0
|
||||
print(line)
|
||||
return lines
|
||||
|
||||
|
||||
def insert_to_db(lines):
|
||||
conn = sqlite3.connect('./leak_passwords.db')
|
||||
cursor = conn.cursor()
|
||||
create_table_sql = '''
|
||||
CREATE TABLE IF NOT EXISTS passwords (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
password CHAR(32)
|
||||
)
|
||||
'''
|
||||
create_index_sql = 'CREATE INDEX IF NOT EXISTS idx_password ON passwords(password)'
|
||||
cursor.execute(create_table_sql)
|
||||
cursor.execute(create_index_sql)
|
||||
|
||||
for line in lines:
|
||||
cursor.execute('INSERT INTO passwords (password) VALUES (?)', [line])
|
||||
conn.commit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filename = sys.argv[1]
|
||||
lines = parse_it(filename)
|
||||
insert_to_db(lines)
|
283
apps/accounts/automations/check_account/manager.py
Normal file
283
apps/accounts/automations/check_account/manager.py
Normal file
@ -0,0 +1,283 @@
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import Account, AccountRisk, RiskChoice
|
||||
from assets.automations.base.manager import BaseManager
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
||||
|
||||
|
||||
@bulk_create_decorator(AccountRisk)
|
||||
def create_risk(data):
|
||||
return AccountRisk(**data)
|
||||
|
||||
|
||||
@bulk_update_decorator(AccountRisk, update_fields=["details", "status"])
|
||||
def update_risk(risk):
|
||||
return risk
|
||||
|
||||
|
||||
class BaseCheckHandler:
|
||||
risk = ''
|
||||
|
||||
def __init__(self, assets):
|
||||
self.assets = assets
|
||||
|
||||
def check(self, account):
|
||||
pass
|
||||
|
||||
def clean(self):
|
||||
pass
|
||||
|
||||
|
||||
class CheckSecretHandler(BaseCheckHandler):
|
||||
risk = RiskChoice.weak_password
|
||||
|
||||
@staticmethod
|
||||
def is_weak_password(password):
|
||||
# 判断密码长度
|
||||
if len(password) < 8:
|
||||
return True
|
||||
|
||||
# 判断是否只有一种字符类型
|
||||
if password.isdigit() or password.isalpha():
|
||||
return True
|
||||
|
||||
# 判断是否只包含数字或字母
|
||||
if password.islower() or password.isupper():
|
||||
return True
|
||||
|
||||
# 判断是否包含常见弱密码
|
||||
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||
if password.lower() in common_passwords:
|
||||
return True
|
||||
|
||||
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||
if (
|
||||
not re.search(r"[A-Za-z]", password)
|
||||
or not re.search(r"[0-9]", password)
|
||||
or not re.search(r"[\W_]", password)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check(self, account):
|
||||
if not account.secret:
|
||||
return False
|
||||
return self.is_weak_password(account.secret)
|
||||
|
||||
|
||||
class CheckRepeatHandler(BaseCheckHandler):
|
||||
risk = RiskChoice.repeated_password
|
||||
|
||||
def __init__(self, assets):
|
||||
super().__init__(assets)
|
||||
self.path, self.conn, self.cursor = self.init_repeat_check_db()
|
||||
self.add_password_for_check_repeat()
|
||||
|
||||
@staticmethod
|
||||
def init_repeat_check_db():
|
||||
path = os.path.join('/tmp', 'accounts_' + str(uuid.uuid4()) + '.db')
|
||||
sql = """
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
digest CHAR(32)
|
||||
)
|
||||
"""
|
||||
index = "CREATE INDEX IF NOT EXISTS idx_digest ON accounts(digest)"
|
||||
conn = sqlite3.connect(path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(sql)
|
||||
cursor.execute(index)
|
||||
return path, conn, cursor
|
||||
|
||||
def check(self, account):
|
||||
if not account.secret:
|
||||
return False
|
||||
|
||||
digest = self.digest(account.secret)
|
||||
sql = 'SELECT COUNT(*) FROM accounts WHERE digest = ?'
|
||||
self.cursor.execute(sql, [digest])
|
||||
result = self.cursor.fetchone()
|
||||
if not result:
|
||||
return False
|
||||
return result[0] > 1
|
||||
|
||||
@staticmethod
|
||||
def digest(secret):
|
||||
return hashlib.md5(secret.encode()).hexdigest()
|
||||
|
||||
def add_password_for_check_repeat(self):
|
||||
accounts = Account.objects.all().only('id', '_secret', 'secret_type')
|
||||
sql = "INSERT INTO accounts (digest) VALUES (?)"
|
||||
|
||||
for account in accounts:
|
||||
secret = account.secret
|
||||
if not secret:
|
||||
continue
|
||||
digest = self.digest(secret)
|
||||
self.cursor.execute(sql, [digest])
|
||||
self.conn.commit()
|
||||
|
||||
def clean(self):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
os.remove(self.path)
|
||||
|
||||
|
||||
class CheckLeakHandler(BaseCheckHandler):
|
||||
risk = RiskChoice.leaked_password
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.conn, self.cursor = self.init_leak_password_db()
|
||||
|
||||
@staticmethod
|
||||
def init_leak_password_db():
|
||||
db_path = os.path.join(
|
||||
settings.APPS_DIR, 'accounts', 'automations',
|
||||
'check_account', 'leak_passwords.db'
|
||||
)
|
||||
|
||||
if settings.LEAK_PASSWORD_DB_PATH and os.path.isfile(settings.LEAK_PASSWORD_DB_PATH):
|
||||
db_path = settings.LEAK_PASSWORD_DB_PATH
|
||||
|
||||
db_conn = sqlite3.connect(db_path)
|
||||
db_cursor = db_conn.cursor()
|
||||
return db_conn, db_cursor
|
||||
|
||||
def check(self, account):
|
||||
if not account.secret:
|
||||
return False
|
||||
|
||||
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1'
|
||||
self.cursor.execute(sql, (account.secret,))
|
||||
leak = self.cursor.fetchone() is not None
|
||||
return leak
|
||||
|
||||
def clean(self):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
|
||||
|
||||
class CheckAccountManager(BaseManager):
|
||||
batch_size = 100
|
||||
tmpl = 'Checked the status of account %s: %s'
|
||||
|
||||
def __init__(self, execution):
|
||||
super().__init__(execution)
|
||||
self.assets = []
|
||||
self.batch_risks = []
|
||||
self.handlers = []
|
||||
|
||||
def add_risk(self, risk, account):
|
||||
self.summary[risk] += 1
|
||||
self.result[risk].append({
|
||||
'asset': str(account.asset), 'username': account.username,
|
||||
})
|
||||
risk_obj = {'account': account, 'risk': risk}
|
||||
self.batch_risks.append(risk_obj)
|
||||
|
||||
def commit_risks(self, assets):
|
||||
account_risks = AccountRisk.objects.filter(asset__in=assets)
|
||||
ori_risk_map = {}
|
||||
|
||||
for risk in account_risks:
|
||||
key = f'{risk.account_id}_{risk.risk}'
|
||||
ori_risk_map[key] = risk
|
||||
|
||||
now = timezone.now().isoformat()
|
||||
for d in self.batch_risks:
|
||||
account = d["account"]
|
||||
key = f'{account.id}_{d["risk"]}'
|
||||
origin_risk = ori_risk_map.get(key)
|
||||
|
||||
if origin_risk and origin_risk.status != ConfirmOrIgnore.pending:
|
||||
details = origin_risk.details or []
|
||||
details.append({"datetime": now, 'type': 'refind'})
|
||||
|
||||
if len(details) > 10:
|
||||
details = [*details[:5], *details[-5:]]
|
||||
|
||||
origin_risk.details = details
|
||||
origin_risk.status = ConfirmOrIgnore.pending
|
||||
update_risk(origin_risk)
|
||||
else:
|
||||
create_risk({
|
||||
"account": account,
|
||||
"asset": account.asset,
|
||||
"username": account.username,
|
||||
"risk": d["risk"],
|
||||
"details": [{"datetime": now, 'type': 'init'}],
|
||||
})
|
||||
|
||||
def pre_run(self):
|
||||
super().pre_run()
|
||||
self.assets = self.execution.get_all_assets()
|
||||
|
||||
def batch_check(self, handler):
|
||||
print("Engine: {}".format(handler.__class__.__name__))
|
||||
for i in range(0, len(self.assets), self.batch_size):
|
||||
_assets = self.assets[i: i + self.batch_size]
|
||||
accounts = Account.objects.filter(asset__in=_assets)
|
||||
|
||||
print("Start to check accounts: {}".format(len(accounts)))
|
||||
|
||||
for account in accounts:
|
||||
error = handler.check(account)
|
||||
msg = handler.risk if error else 'ok'
|
||||
|
||||
print("Check: {} => {}".format(account, msg))
|
||||
if not error:
|
||||
continue
|
||||
self.add_risk(handler.risk, account)
|
||||
self.commit_risks(_assets)
|
||||
|
||||
def do_run(self, *args, **kwargs):
|
||||
engines = self.execution.snapshot.get("engines", [])
|
||||
if engines == '__all__':
|
||||
engines = ['check_account_secret', 'check_account_repeat', 'check_account_leak']
|
||||
|
||||
for engine in engines:
|
||||
if engine == "check_account_secret":
|
||||
handler = CheckSecretHandler(self.assets)
|
||||
elif engine == "check_account_repeat":
|
||||
handler = CheckRepeatHandler(self.assets)
|
||||
elif engine == "check_account_leak":
|
||||
handler = CheckLeakHandler(self.assets)
|
||||
else:
|
||||
print("Unknown engine: {}".format(engine))
|
||||
continue
|
||||
|
||||
self.handlers.append(handler)
|
||||
self.batch_check(handler)
|
||||
|
||||
def post_run(self):
|
||||
super().post_run()
|
||||
for handler in self.handlers:
|
||||
handler.clean()
|
||||
|
||||
def get_report_subject(self):
|
||||
return "Check account report of %s" % self.execution.id
|
||||
|
||||
def get_report_template(self):
|
||||
return "accounts/check_account_report.html"
|
||||
|
||||
def print_summary(self):
|
||||
tmpl = _("---\nSummary: \nok: {}, weak password: {}, leaked password: {}, "
|
||||
"repeated password: {}, no secret: {}, using time: {}s").format(
|
||||
self.summary["ok"],
|
||||
self.summary[RiskChoice.weak_password],
|
||||
self.summary[RiskChoice.leaked_password],
|
||||
self.summary[RiskChoice.repeated_password],
|
||||
self.summary["no_secret"],
|
||||
self.duration
|
||||
)
|
||||
print(tmpl)
|
@ -1,6 +1,7 @@
|
||||
from .backup_account.manager import AccountBackupManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .gather_accounts.manager import GatherAccountsManager
|
||||
from .check_account.manager import CheckAccountManager
|
||||
from .gather_account.manager import GatherAccountsManager
|
||||
from .push_account.manager import PushAccountManager
|
||||
from .remove_account.manager import RemoveAccountManager
|
||||
from .verify_account.manager import VerifyAccountManager
|
||||
@ -16,8 +17,8 @@ class ExecutionManager:
|
||||
AutomationTypes.remove_account: RemoveAccountManager,
|
||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
||||
# TODO 后期迁移到自动化策略中
|
||||
'backup_account': AccountBackupManager,
|
||||
AutomationTypes.check_account: CheckAccountManager,
|
||||
AutomationTypes.backup_account: AccountBackupManager,
|
||||
}
|
||||
|
||||
def __init__(self, execution):
|
||||
@ -26,3 +27,6 @@ class ExecutionManager:
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
return self._runner.run(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._runner, item)
|
||||
|
@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@ -15,7 +15,7 @@
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
@ -1,8 +1,11 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
- hosts: oralce
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
@ -0,0 +1,30 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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
|
||||
community.postgresql.postgresql_info:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
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
|
||||
|
||||
- name: Define info by set_fact
|
||||
set_fact:
|
||||
info: "{{ db_info.roles }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -0,0 +1,43 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT
|
||||
l.name,
|
||||
l.modify_date,
|
||||
l.is_disabled,
|
||||
l.create_date,
|
||||
l.default_database_name,
|
||||
LOGINPROPERTY(name, 'DaysUntilExpiration') AS days_until_expiration,
|
||||
MAX(s.login_time) AS last_login_time
|
||||
FROM
|
||||
sys.sql_logins l
|
||||
LEFT JOIN
|
||||
sys.dm_exec_sessions s
|
||||
ON
|
||||
l.name = s.login_name
|
||||
WHERE
|
||||
s.is_user_process = 1 OR s.login_name IS NULL
|
||||
GROUP BY
|
||||
l.name, l.create_date, l.modify_date, l.is_disabled, l.default_database_name
|
||||
ORDER BY
|
||||
last_login_time DESC;
|
||||
output: dict
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
set_fact:
|
||||
info: "{{ db_info.query_results_dict }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -0,0 +1,10 @@
|
||||
id: gather_accounts_sqlserver
|
||||
name: "{{ 'SQLServer account gather' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: gather_accounts
|
||||
i18n:
|
||||
SQLServer account gather:
|
||||
zh: SQLServer 账号收集
|
||||
ja: SQLServer アカウントの収集
|
270
apps/accounts/automations/gather_account/filter.py
Normal file
270
apps/accounts/automations/gather_account/filter.py
Normal file
@ -0,0 +1,270 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
__all__ = ['GatherAccountsFilter']
|
||||
|
||||
|
||||
def parse_date(date_str, default=None):
|
||||
if not date_str:
|
||||
return default
|
||||
if date_str in ['Never', 'null']:
|
||||
return default
|
||||
formats = [
|
||||
'%Y/%m/%d %H:%M:%S',
|
||||
'%Y-%m-%dT%H:%M:%S',
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%d-%m-%Y %H:%M:%S',
|
||||
'%Y/%m/%d',
|
||||
'%d-%m-%Y',
|
||||
]
|
||||
for fmt in formats:
|
||||
try:
|
||||
dt = datetime.strptime(date_str, fmt)
|
||||
return timezone.make_aware(dt, timezone.get_current_timezone())
|
||||
except ValueError:
|
||||
continue
|
||||
return default
|
||||
|
||||
|
||||
class GatherAccountsFilter:
|
||||
def __init__(self, tp):
|
||||
self.tp = tp
|
||||
|
||||
@staticmethod
|
||||
def mysql_filter(info):
|
||||
result = {}
|
||||
for host, user_dict in info.items():
|
||||
for username, user_info in user_dict.items():
|
||||
password_last_changed = parse_date(user_info.get('password_last_changed'))
|
||||
password_lifetime = user_info.get('password_lifetime')
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': password_last_changed,
|
||||
'date_password_expired': password_last_changed + timezone.timedelta(
|
||||
days=password_lifetime) if password_last_changed and password_lifetime else None,
|
||||
'date_last_login': None,
|
||||
'groups': '',
|
||||
}
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def postgresql_filter(info):
|
||||
result = {}
|
||||
for username, user_info in info.items():
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': None,
|
||||
'date_password_expired': parse_date(user_info.get('valid_until')),
|
||||
'date_last_login': None,
|
||||
'groups': '',
|
||||
}
|
||||
detail = {
|
||||
'can_login': user_info.get('canlogin'),
|
||||
'superuser': user_info.get('superuser'),
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def sqlserver_filter(info):
|
||||
if not info:
|
||||
return {}
|
||||
result = {}
|
||||
for user_info in info[0][0]:
|
||||
days_until_expiration = user_info.get('days_until_expiration')
|
||||
date_password_expired = timezone.now() + timezone.timedelta(
|
||||
days=int(days_until_expiration)) if days_until_expiration else None
|
||||
user = {
|
||||
'username': user_info.get('name', ''),
|
||||
'date_password_change': parse_date(user_info.get('modify_date')),
|
||||
'date_password_expired': date_password_expired,
|
||||
'date_last_login': parse_date(user_info.get('last_login_time')),
|
||||
'groups': '',
|
||||
}
|
||||
detail = {
|
||||
'create_date': user_info.get('create_date', ''),
|
||||
'is_disabled': user_info.get('is_disabled', ''),
|
||||
'default_database_name': user_info.get('default_database_name', ''),
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[user['username']] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def oracle_filter(info):
|
||||
result = {}
|
||||
for default_tablespace, users in info.items():
|
||||
for username, user_info in users.items():
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': parse_date(user_info.get('password_change_date')),
|
||||
'date_password_expired': parse_date(user_info.get('expiry_date')),
|
||||
'date_last_login': parse_date(user_info.get('last_login')),
|
||||
'groups': '',
|
||||
}
|
||||
detail = {
|
||||
'uid': user_info.get('user_id', ''),
|
||||
'create_date': user_info.get('created', ''),
|
||||
'account_status': user_info.get('account_status', ''),
|
||||
'default_tablespace': default_tablespace,
|
||||
'roles': user_info.get('roles', []),
|
||||
'privileges': user_info.get('privileges', []),
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[user['username']] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def posix_filter(info):
|
||||
user_groups = info.pop('user_groups', [])
|
||||
username_groups = {}
|
||||
for line in user_groups:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, groups = line.split(':', 1)
|
||||
username_groups[username.strip()] = groups.strip()
|
||||
|
||||
user_sudo = info.pop('user_sudo', [])
|
||||
username_sudo = {}
|
||||
for line in user_sudo:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, sudo = line.split(':', 1)
|
||||
if not sudo.strip():
|
||||
continue
|
||||
username_sudo[username.strip()] = sudo.strip()
|
||||
|
||||
last_login = info.pop('last_login', '')
|
||||
user_last_login = {}
|
||||
for line in last_login:
|
||||
if not line.strip() or ' ' not in line:
|
||||
continue
|
||||
username, login = line.split(' ', 1)
|
||||
user_last_login[username] = login.split()
|
||||
|
||||
user_authorized = info.pop('user_authorized', [])
|
||||
username_authorized = {}
|
||||
for line in user_authorized:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, authorized = line.split(':', 1)
|
||||
username_authorized[username.strip()] = authorized.strip()
|
||||
|
||||
passwd_date = info.pop('passwd_date', [])
|
||||
username_password_date = {}
|
||||
for line in passwd_date:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, password_date = line.split(':', 1)
|
||||
username_password_date[username.strip()] = password_date.strip().split()
|
||||
|
||||
result = {}
|
||||
users = info.pop('users', '')
|
||||
|
||||
for username in users:
|
||||
if not username:
|
||||
continue
|
||||
user = dict()
|
||||
|
||||
login = user_last_login.get(username) or ''
|
||||
if login and len(login) == 3:
|
||||
user['address_last_login'] = login[0][:32]
|
||||
try:
|
||||
login_date = timezone.datetime.fromisoformat(login[1])
|
||||
user['date_last_login'] = login_date
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
start_date = timezone.make_aware(timezone.datetime(1970, 1, 1))
|
||||
_password_date = username_password_date.get(username) or ''
|
||||
if _password_date and len(_password_date) == 2:
|
||||
if _password_date[0]:
|
||||
user['date_password_change'] = start_date + timezone.timedelta(days=int(_password_date[0]))
|
||||
if _password_date[1]:
|
||||
user['date_password_expired'] = start_date + timezone.timedelta(days=int(_password_date[1]))
|
||||
detail = {
|
||||
'groups': username_groups.get(username) or '',
|
||||
'sudoers': username_sudo.get(username) or '',
|
||||
'authorized_keys': username_authorized.get(username) or ''
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def windows_filter(info):
|
||||
result = {}
|
||||
for user_details in info['user_details']:
|
||||
user_info = {}
|
||||
lines = user_details['stdout_lines']
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split(' ', 1)
|
||||
if len(parts) == 2:
|
||||
key, value = parts
|
||||
user_info[key.strip()] = value.strip()
|
||||
detail = {'groups': user_info.get('Global Group memberships', ''), }
|
||||
|
||||
username = user_info.get('User name')
|
||||
if not username:
|
||||
continue
|
||||
|
||||
result[username] = {
|
||||
'username': username,
|
||||
'date_password_change': parse_date(user_info.get('Password last set')),
|
||||
'date_password_expired': parse_date(user_info.get('Password expires')),
|
||||
'date_last_login': parse_date(user_info.get('Last logon')),
|
||||
'groups': detail,
|
||||
}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def windows_ad_filter(info):
|
||||
result = {}
|
||||
for user_info in info['user_details']:
|
||||
detail = {'groups': user_info.get('GlobalGroupMemberships', ''), }
|
||||
username = user_info.get('SamAccountName')
|
||||
if not username:
|
||||
continue
|
||||
result[username] = {
|
||||
'username': username,
|
||||
'date_password_change': parse_date(user_info.get('PasswordLastSet')),
|
||||
'date_password_expired': parse_date(user_info.get('PasswordExpires')),
|
||||
'date_last_login': parse_date(user_info.get('LastLogonDate')),
|
||||
'groups': detail,
|
||||
}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def mongodb_filter(info):
|
||||
result = {}
|
||||
for db, users in info.items():
|
||||
for username, user_info in users.items():
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': None,
|
||||
'date_password_expired': None,
|
||||
'date_last_login': None,
|
||||
'groups': '',
|
||||
}
|
||||
result['detail'] = {'db': db, 'roles': user_info.get('roles', [])}
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
def run(self, method_id_meta_mapper, info):
|
||||
run_method_name = None
|
||||
for k, v in method_id_meta_mapper.items():
|
||||
if self.tp not in v['type']:
|
||||
continue
|
||||
run_method_name = k.replace(f'{v["method"]}_', '')
|
||||
|
||||
if not run_method_name:
|
||||
return info
|
||||
|
||||
if hasattr(self, f'{run_method_name}_filter'):
|
||||
return getattr(self, f'{run_method_name}_filter')(info)
|
||||
return info
|
61
apps/accounts/automations/gather_account/host/posix/main.yml
Normal file
61
apps/accounts/automations/gather_account/host/posix/main.yml
Normal file
@ -0,0 +1,61 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Get users
|
||||
ansible.builtin.shell:
|
||||
cmd: >
|
||||
getent passwd | awk -F: '$7 !~ /(false|nologin|true|sync)$/' | grep -v '^$' | awk -F":" '{ print $1 }'
|
||||
register: users
|
||||
|
||||
- name: Gather posix account last login
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
last -wi --time-format iso -n 1 ${user} | awk '{ print $1,$3,$4, $NF }' | head -1 | awk 'NF'
|
||||
done
|
||||
register: last_login
|
||||
|
||||
- name: Get user password change date and expiry
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
k=$(getent shadow $user | awk -F: '{ print $3, $5 }')
|
||||
echo "$user:$k"
|
||||
done
|
||||
register: passwd_date
|
||||
|
||||
- name: Get user groups
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
echo "$(groups $user)" | sed 's@ : @:@g'
|
||||
done
|
||||
register: user_groups
|
||||
|
||||
- name: Get sudoers
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
echo "$user: $(grep "^$user " /etc/sudoers | tr '\n' ';' || echo '')"
|
||||
done
|
||||
register: user_sudo
|
||||
|
||||
- name: Get authorized keys
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
home=$(getent passwd $user | cut -d: -f6)
|
||||
echo -n "$user:"
|
||||
if [ -f "${home}/.ssh/authorized_keys" ]; then
|
||||
cat ${home}/.ssh/authorized_keys | tr '\n' ';'
|
||||
fi
|
||||
echo
|
||||
done
|
||||
register: user_authorized
|
||||
|
||||
- set_fact:
|
||||
info:
|
||||
users: "{{ users.stdout_lines }}"
|
||||
last_login: "{{ last_login.stdout_lines }}"
|
||||
user_groups: "{{ user_groups.stdout_lines }}"
|
||||
user_sudo: "{{ user_sudo.stdout_lines }}"
|
||||
user_authorized: "{{ user_authorized.stdout_lines }}"
|
||||
passwd_date: "{{ passwd_date.stdout_lines }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -0,0 +1,33 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Run net user command to get all users
|
||||
win_shell: net user
|
||||
register: user_list_output
|
||||
failed_when: false
|
||||
|
||||
- name: Parse all users from net user command
|
||||
set_fact:
|
||||
all_users: >-
|
||||
{%- set users = [] -%}
|
||||
{%- for line in user_list_output.stdout_lines -%}
|
||||
{%- if loop.index > 4 and line.strip() != "" and not line.startswith("The command completed") -%}
|
||||
{%- for user in line.split() -%}
|
||||
{%- set _ = users.append(user) -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ users }}
|
||||
|
||||
- name: Run net user command for each user to get details
|
||||
win_shell: net user {{ item }}
|
||||
loop: "{{ all_users }}"
|
||||
register: user_details
|
||||
ignore_errors: yes
|
||||
|
||||
- set_fact:
|
||||
info:
|
||||
user_details: "{{ user_details.results }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -2,10 +2,13 @@ id: gather_accounts_windows
|
||||
name: "{{ 'Windows account gather' | trans }}"
|
||||
version: 1
|
||||
method: gather_accounts
|
||||
category: host
|
||||
category:
|
||||
- host
|
||||
|
||||
type:
|
||||
- windows
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account gather:
|
||||
zh: 使用命令 net user 收集 Windows 账号
|
@ -0,0 +1,74 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Import ActiveDirectory module
|
||||
win_shell: Import-Module ActiveDirectory
|
||||
args:
|
||||
warn: false
|
||||
|
||||
- name: Get the SamAccountName list of all AD users
|
||||
win_shell: |
|
||||
Import-Module ActiveDirectory
|
||||
Get-ADUser -Filter * | Select-Object -ExpandProperty SamAccountName
|
||||
register: ad_user_list
|
||||
|
||||
- name: Set the all_users variable
|
||||
set_fact:
|
||||
all_users: "{{ ad_user_list.stdout_lines }}"
|
||||
|
||||
- name: Get detailed information for each user
|
||||
win_shell: |
|
||||
Import-Module ActiveDirectory
|
||||
|
||||
$user = Get-ADUser -Identity {{ item }} -Properties Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet, msDS-UserPasswordExpiryTimeComputed, MemberOf
|
||||
|
||||
$globalGroups = @()
|
||||
if ($user.MemberOf) {
|
||||
$globalGroups = $user.MemberOf | ForEach-Object {
|
||||
try {
|
||||
$group = Get-ADGroup $_ -ErrorAction Stop
|
||||
if ($group.GroupScope -eq 'Global') { $group.Name }
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$passwordExpiry = $null
|
||||
$expiryRaw = $user.'msDS-UserPasswordExpiryTimeComputed'
|
||||
if ($expiryRaw) {
|
||||
try {
|
||||
$passwordExpiry = [datetime]::FromFileTime($expiryRaw)
|
||||
} catch {
|
||||
$passwordExpiry = $null
|
||||
}
|
||||
}
|
||||
|
||||
$output = [PSCustomObject]@{
|
||||
Name = $user.Name
|
||||
SamAccountName = $user.SamAccountName
|
||||
Enabled = $user.Enabled
|
||||
LastLogonDate = if ($user.LastLogonDate) { $user.LastLogonDate.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
|
||||
PasswordLastSet = if ($user.PasswordLastSet) { $user.PasswordLastSet.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
|
||||
PasswordExpires = if ($passwordExpiry) { $passwordExpiry.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
|
||||
GlobalGroupMemberships = $globalGroups
|
||||
}
|
||||
|
||||
$output | ConvertTo-Json -Depth 3
|
||||
loop: "{{ all_users }}"
|
||||
register: ad_user_details
|
||||
ignore_errors: yes
|
||||
|
||||
|
||||
- set_fact:
|
||||
info:
|
||||
user_details: >-
|
||||
{{
|
||||
ad_user_details.results
|
||||
| selectattr('rc', 'equalto', 0)
|
||||
| map(attribute='stdout')
|
||||
| select('truthy')
|
||||
| map('from_json')
|
||||
}}
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -0,0 +1,15 @@
|
||||
id: gather_accounts_windows_ad
|
||||
name: "{{ 'Windows account gather' | trans }}"
|
||||
version: 1
|
||||
method: gather_accounts
|
||||
category:
|
||||
- ds
|
||||
|
||||
type:
|
||||
- windows_ad
|
||||
|
||||
i18n:
|
||||
Windows account gather:
|
||||
zh: 使用命令 Get-ADUser 收集 Windows 账号
|
||||
ja: コマンド Get-ADUser を使用して Windows アカウントを収集する
|
||||
en: Using command Get-ADUser to gather accounts
|
409
apps/accounts/automations/gather_account/manager.py
Normal file
409
apps/accounts/automations/gather_account/manager.py
Normal file
@ -0,0 +1,409 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import GatheredAccount, Account, AccountRisk, RiskChoice
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
||||
from common.utils import get_logger
|
||||
from common.utils.strings import get_text_diff
|
||||
from orgs.utils import tmp_to_org
|
||||
from .filter import GatherAccountsFilter
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
risk_items = [
|
||||
"authorized_keys",
|
||||
"sudoers",
|
||||
"groups",
|
||||
]
|
||||
common_risk_items = [
|
||||
"address_last_login",
|
||||
"date_last_login",
|
||||
"date_password_change",
|
||||
"date_password_expired",
|
||||
"detail"
|
||||
]
|
||||
diff_items = risk_items + common_risk_items
|
||||
|
||||
|
||||
def format_datetime(value):
|
||||
if isinstance(value, timezone.datetime):
|
||||
return value.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return value
|
||||
|
||||
|
||||
def get_items_diff(ori_account, d):
|
||||
if hasattr(ori_account, "_diff"):
|
||||
return ori_account._diff
|
||||
|
||||
diff = {}
|
||||
for item in diff_items:
|
||||
get_item_diff(item, ori_account, d, diff)
|
||||
ori_account._diff = diff
|
||||
return diff
|
||||
|
||||
|
||||
def get_item_diff(item, ori_account, d, diff):
|
||||
detail = getattr(ori_account, 'detail', {})
|
||||
new_detail = d.get('detail', {})
|
||||
ori = getattr(ori_account, item, None) or detail.get(item)
|
||||
new = d.get(item, "") or new_detail.get(item)
|
||||
if not ori and not new:
|
||||
return
|
||||
|
||||
ori = format_datetime(ori)
|
||||
new = format_datetime(new)
|
||||
|
||||
if new != ori:
|
||||
diff[item] = get_text_diff(str(ori), str(new))
|
||||
|
||||
|
||||
class AnalyseAccountRisk:
|
||||
long_time = timezone.timedelta(days=90)
|
||||
datetime_check_items = [
|
||||
{"field": "date_last_login", "risk": "long_time_no_login", "delta": long_time},
|
||||
{
|
||||
"field": "date_password_change",
|
||||
"risk": RiskChoice.long_time_password,
|
||||
"delta": long_time,
|
||||
},
|
||||
{
|
||||
"field": "date_password_expired",
|
||||
"risk": "password_expired",
|
||||
"delta": timezone.timedelta(seconds=1),
|
||||
},
|
||||
]
|
||||
|
||||
def __init__(self, check_risk=True):
|
||||
self.check_risk = check_risk
|
||||
self.now = timezone.now()
|
||||
self.pending_add_risks = []
|
||||
|
||||
def _analyse_item_changed(self, ori_ga, d):
|
||||
diff = get_items_diff(ori_ga, d)
|
||||
if not diff:
|
||||
return
|
||||
|
||||
risks = []
|
||||
for k, v in diff.items():
|
||||
if k not in risk_items:
|
||||
continue
|
||||
risks.append(
|
||||
dict(
|
||||
asset_id=str(ori_ga.asset_id),
|
||||
username=ori_ga.username,
|
||||
gathered_account=ori_ga,
|
||||
risk=k + "_changed",
|
||||
detail={"diff": v},
|
||||
)
|
||||
)
|
||||
self.save_or_update_risks(risks)
|
||||
|
||||
def _analyse_datetime_changed(self, ori_account, d, asset, username):
|
||||
basic = {"asset_id": str(asset.id), "username": username}
|
||||
|
||||
risks = []
|
||||
for item in self.datetime_check_items:
|
||||
field = item["field"]
|
||||
risk = item["risk"]
|
||||
delta = item["delta"]
|
||||
|
||||
date = d.get(field)
|
||||
if not date:
|
||||
continue
|
||||
|
||||
# 服务器收集的时间和数据库时间一致,不进行比较,无法检测风险 不太对,先注释
|
||||
# pre_date = ori_account and getattr(ori_account, field)
|
||||
# if pre_date == date:
|
||||
# continue
|
||||
|
||||
if date and date < timezone.now() - delta:
|
||||
risks.append(
|
||||
dict(**basic, risk=risk, detail={"date": date.isoformat()})
|
||||
)
|
||||
|
||||
self.save_or_update_risks(risks)
|
||||
|
||||
def save_or_update_risks(self, risks):
|
||||
# 提前取出来,避免每次都查数据库
|
||||
asset_ids = {r["asset_id"] for r in risks}
|
||||
assets_risks = AccountRisk.objects.filter(asset_id__in=asset_ids)
|
||||
assets_risks = {f"{r.asset_id}_{r.username}_{r.risk}": r for r in assets_risks}
|
||||
|
||||
for d in risks:
|
||||
detail = d.pop("detail", {})
|
||||
detail["datetime"] = self.now.isoformat()
|
||||
key = f"{d['asset_id']}_{d['username']}_{d['risk']}"
|
||||
found = assets_risks.get(key)
|
||||
|
||||
if not found:
|
||||
self._create_risk(dict(**d, details=[detail]))
|
||||
continue
|
||||
|
||||
found.details.append(detail)
|
||||
self._update_risk(found)
|
||||
|
||||
@bulk_create_decorator(AccountRisk)
|
||||
def _create_risk(self, data):
|
||||
return AccountRisk(**data)
|
||||
|
||||
@bulk_update_decorator(AccountRisk, update_fields=["details"])
|
||||
def _update_risk(self, account):
|
||||
return account
|
||||
|
||||
def lost_accounts(self, asset, lost_users):
|
||||
if not self.check_risk:
|
||||
return
|
||||
for user in lost_users:
|
||||
self._create_risk(
|
||||
dict(
|
||||
asset_id=str(asset.id),
|
||||
username=user,
|
||||
risk=RiskChoice.account_deleted,
|
||||
details=[{"datetime": self.now.isoformat()}],
|
||||
)
|
||||
)
|
||||
|
||||
def analyse_risk(self, asset, ga, d, sys_found):
|
||||
if not self.check_risk:
|
||||
return
|
||||
|
||||
if ga:
|
||||
self._analyse_item_changed(ga, d)
|
||||
if not sys_found:
|
||||
basic = {"asset": asset, "username": d["username"], 'gathered_account': ga}
|
||||
self._create_risk(
|
||||
dict(
|
||||
**basic,
|
||||
risk=RiskChoice.new_found,
|
||||
details=[{"datetime": self.now.isoformat()}],
|
||||
)
|
||||
)
|
||||
self._analyse_datetime_changed(ga, d, asset, d["username"])
|
||||
|
||||
|
||||
class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_asset_mapper = {}
|
||||
self.asset_account_info = {}
|
||||
self.asset_usernames_mapper = defaultdict(set)
|
||||
self.ori_asset_usernames = defaultdict(set)
|
||||
self.ori_gathered_usernames = defaultdict(set)
|
||||
self.ori_gathered_accounts_mapper = dict()
|
||||
self.is_sync_account = self.execution.snapshot.get("is_sync_account")
|
||||
self.check_risk = self.execution.snapshot.get("check_risk", False)
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.gather_accounts
|
||||
|
||||
def host_callback(self, host, asset=None, **kwargs):
|
||||
super().host_callback(host, asset=asset, **kwargs)
|
||||
self.host_asset_mapper[host["name"]] = asset
|
||||
return host
|
||||
|
||||
def _filter_success_result(self, tp, result):
|
||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_nested_info(data, *keys):
|
||||
for key in keys:
|
||||
data = data.get(key, {})
|
||||
if not data:
|
||||
break
|
||||
return data
|
||||
|
||||
def _collect_asset_account_info(self, asset, info):
|
||||
result = self._filter_success_result(asset.type, info)
|
||||
accounts = []
|
||||
|
||||
for username, info in result.items():
|
||||
self.asset_usernames_mapper[str(asset.id)].add(username)
|
||||
|
||||
d = {"asset": asset, "username": username, "remote_present": True, **info}
|
||||
accounts.append(d)
|
||||
self.asset_account_info[asset] = accounts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
super().on_host_success(host, result)
|
||||
info = self._get_nested_info(result, "debug", "res", "info")
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
|
||||
if asset and info:
|
||||
self._collect_asset_account_info(asset, info)
|
||||
else:
|
||||
print(f"\033[31m Not found {host} info \033[0m\n")
|
||||
|
||||
def prefetch_origin_account_usernames(self):
|
||||
"""
|
||||
提起查出来,避免每次 sql 查询
|
||||
:return:
|
||||
"""
|
||||
assets = self.asset_usernames_mapper.keys()
|
||||
accounts = Account.objects.filter(asset__in=assets).values_list(
|
||||
"asset", "username"
|
||||
)
|
||||
|
||||
for asset_id, username in accounts:
|
||||
self.ori_asset_usernames[str(asset_id)].add(username)
|
||||
|
||||
ga_accounts = GatheredAccount.objects.filter(asset__in=assets)
|
||||
for account in ga_accounts:
|
||||
self.ori_gathered_usernames[str(account.asset_id)].add(account.username)
|
||||
key = "{}_{}".format(account.asset_id, account.username)
|
||||
self.ori_gathered_accounts_mapper[key] = account
|
||||
|
||||
def update_gather_accounts_status(self, asset):
|
||||
"""
|
||||
远端账号,收集中的账号,vault 中的账号。
|
||||
要根据账号新增见啥,标识 收集账号的状态, 让管理员关注
|
||||
|
||||
远端账号 -> 收集账号 -> 特权账号
|
||||
"""
|
||||
remote_users = self.asset_usernames_mapper[str(asset.id)]
|
||||
ori_users = self.ori_asset_usernames[str(asset.id)]
|
||||
ori_ga_users = self.ori_gathered_usernames[str(asset.id)]
|
||||
|
||||
queryset = GatheredAccount.objects.filter(asset=asset).exclude(
|
||||
status=ConfirmOrIgnore.ignored
|
||||
)
|
||||
|
||||
# 远端账号 比 收集账号多的
|
||||
# 新增创建,不用处理状态
|
||||
new_found_users = remote_users - ori_ga_users
|
||||
if new_found_users:
|
||||
self.summary["new_accounts"] += len(new_found_users)
|
||||
for username in new_found_users:
|
||||
self.result["new_accounts"].append(
|
||||
{
|
||||
"asset": str(asset),
|
||||
"username": username,
|
||||
}
|
||||
)
|
||||
|
||||
# 远端上 比 收集账号少的
|
||||
# 标识 remote_present=False, 标记为待处理
|
||||
# 远端资产上不存在的,标识为待处理,需要管理员介入
|
||||
lost_users = ori_ga_users - remote_users
|
||||
if lost_users:
|
||||
queryset.filter(username__in=lost_users).update(
|
||||
status=ConfirmOrIgnore.pending, remote_present=False
|
||||
)
|
||||
self.summary["lost_accounts"] += len(lost_users)
|
||||
for username in lost_users:
|
||||
self.result["lost_accounts"].append(
|
||||
{
|
||||
"asset": str(asset),
|
||||
"username": username,
|
||||
}
|
||||
)
|
||||
risk_analyser = AnalyseAccountRisk(self.check_risk)
|
||||
risk_analyser.lost_accounts(asset, lost_users)
|
||||
|
||||
# 收集的账号 比 账号列表多的, 有可能是账号中删掉了, 但这时候状态已经是 confirm 了
|
||||
# 标识状态为 待处理, 让管理员去确认
|
||||
ga_added_users = ori_ga_users - ori_users
|
||||
if ga_added_users:
|
||||
queryset.filter(username__in=ga_added_users).update(status=ConfirmOrIgnore.pending)
|
||||
|
||||
# 收集的账号 比 账号列表少的
|
||||
# 这个好像不不用对比,原始情况就这样
|
||||
|
||||
# 远端账号 比 账号列表少的
|
||||
# 创建收集账号,标识 remote_present=False, 状态待处理
|
||||
|
||||
# 远端账号 比 账号列表多的
|
||||
# 正常情况, 不用处理,因为远端账号会创建到收集账号,收集账号再去对比
|
||||
|
||||
# 不过这个好像也处理一下 status,因为已存在,这是状态应该是确认
|
||||
(
|
||||
queryset.filter(username__in=ori_users)
|
||||
.exclude(status=ConfirmOrIgnore.confirmed)
|
||||
.update(status=ConfirmOrIgnore.confirmed)
|
||||
)
|
||||
|
||||
# 远端存在的账号,标识为已存在
|
||||
(
|
||||
queryset.filter(username__in=remote_users, remote_present=False).update(
|
||||
remote_present=True
|
||||
)
|
||||
)
|
||||
|
||||
# 资产上没有的,标识为为存在
|
||||
(
|
||||
queryset.exclude(username__in=ori_users)
|
||||
.filter(present=True)
|
||||
.update(present=False)
|
||||
)
|
||||
(
|
||||
queryset.filter(username__in=ori_users)
|
||||
.filter(present=False)
|
||||
.update(present=True)
|
||||
)
|
||||
|
||||
@bulk_create_decorator(GatheredAccount)
|
||||
def create_gathered_account(self, d):
|
||||
ga = GatheredAccount()
|
||||
for k, v in d.items():
|
||||
setattr(ga, k, v)
|
||||
|
||||
return ga
|
||||
|
||||
@bulk_update_decorator(GatheredAccount, update_fields=common_risk_items)
|
||||
def update_gathered_account(self, ori_account, d):
|
||||
diff = get_items_diff(ori_account, d)
|
||||
if not diff:
|
||||
return
|
||||
for k in diff:
|
||||
if k not in common_risk_items:
|
||||
continue
|
||||
v = d.get(k)
|
||||
setattr(ori_account, k, v)
|
||||
return ori_account
|
||||
|
||||
def do_run(self, *args, **kwargs):
|
||||
super().do_run(*args, **kwargs)
|
||||
self.prefetch_origin_account_usernames()
|
||||
risk_analyser = AnalyseAccountRisk(self.check_risk)
|
||||
|
||||
for asset, accounts_data in self.asset_account_info.items():
|
||||
ori_users = self.ori_asset_usernames[str(asset.id)]
|
||||
need_analyser_gather_account = []
|
||||
with tmp_to_org(asset.org_id):
|
||||
for d in accounts_data:
|
||||
username = d["username"]
|
||||
ori_account = self.ori_gathered_accounts_mapper.get(
|
||||
"{}_{}".format(asset.id, username)
|
||||
)
|
||||
if not ori_account:
|
||||
ga = self.create_gathered_account(d)
|
||||
else:
|
||||
ga = ori_account
|
||||
self.update_gathered_account(ori_account, d)
|
||||
ori_found = username in ori_users
|
||||
need_analyser_gather_account.append((asset, ga, d, ori_found))
|
||||
self.create_gathered_account.finish()
|
||||
self.update_gathered_account.finish()
|
||||
for analysis_data in need_analyser_gather_account:
|
||||
risk_analyser.analyse_risk(*analysis_data)
|
||||
self.update_gather_accounts_status(asset)
|
||||
if not self.is_sync_account:
|
||||
continue
|
||||
gathered_accounts = GatheredAccount.objects.filter(asset=asset)
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
GatheredAccount.objects.filter(
|
||||
asset=asset, username__in=ori_users, present=False
|
||||
).update(
|
||||
present=True
|
||||
)
|
||||
# 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步
|
||||
time.sleep(0.5)
|
||||
|
||||
def get_report_template(self):
|
||||
return "accounts/gather_account_report.html"
|
@ -1,22 +0,0 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
community.postgresql.postgresql_info:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
filter: "roles"
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
set_fact:
|
||||
info: "{{ db_info.roles }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -1,74 +0,0 @@
|
||||
import re
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
__all__ = ['GatherAccountsFilter']
|
||||
|
||||
|
||||
# TODO 后期会挪到playbook中
|
||||
class GatherAccountsFilter:
|
||||
|
||||
def __init__(self, tp):
|
||||
self.tp = tp
|
||||
|
||||
@staticmethod
|
||||
def mysql_filter(info):
|
||||
result = {}
|
||||
for _, user_dict in info.items():
|
||||
for username, _ in user_dict.items():
|
||||
if len(username.split('.')) == 1:
|
||||
result[username] = {}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def postgresql_filter(info):
|
||||
result = {}
|
||||
for username in info:
|
||||
result[username] = {}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def posix_filter(info):
|
||||
username_pattern = re.compile(r'^(\S+)')
|
||||
ip_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
|
||||
login_time_pattern = re.compile(r'\w{3} \d{2} \d{2}:\d{2}:\d{2} \d{4}')
|
||||
result = {}
|
||||
for line in info:
|
||||
usernames = username_pattern.findall(line)
|
||||
username = ''.join(usernames)
|
||||
if username:
|
||||
result[username] = {}
|
||||
else:
|
||||
continue
|
||||
ip_addrs = ip_pattern.findall(line)
|
||||
ip_addr = ''.join(ip_addrs)
|
||||
if ip_addr:
|
||||
result[username].update({'address': ip_addr})
|
||||
login_times = login_time_pattern.findall(line)
|
||||
if login_times:
|
||||
date = timezone.datetime.strptime(f'{login_times[0]} +0800', '%b %d %H:%M:%S %Y %z')
|
||||
result[username].update({'date': date})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def windows_filter(info):
|
||||
info = info[4:-2]
|
||||
result = {}
|
||||
for i in info:
|
||||
for username in i.split():
|
||||
result[username] = {}
|
||||
return result
|
||||
|
||||
def run(self, method_id_meta_mapper, info):
|
||||
run_method_name = None
|
||||
for k, v in method_id_meta_mapper.items():
|
||||
if self.tp not in v['type']:
|
||||
continue
|
||||
run_method_name = k.replace(f'{v["method"]}_', '')
|
||||
|
||||
if not run_method_name:
|
||||
return info
|
||||
|
||||
if hasattr(self, f'{run_method_name}_filter'):
|
||||
return getattr(self, f'{run_method_name}_filter')(info)
|
||||
return info
|
@ -1,21 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Gather posix account
|
||||
ansible.builtin.shell:
|
||||
cmd: >
|
||||
users=$(getent passwd | grep -v nologin | grep -v shutdown | awk -F":" '{ print $1 }');for i in $users;
|
||||
do k=$(last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $0 }')
|
||||
if [ -n "$k" ]; then
|
||||
echo $k
|
||||
else
|
||||
echo $i
|
||||
fi;done
|
||||
register: result
|
||||
|
||||
- name: Define info by set_fact
|
||||
ansible.builtin.set_fact:
|
||||
info: "{{ result.stdout_lines }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -1,14 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Gather windows account
|
||||
ansible.builtin.win_shell: net user
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Define info by set_fact
|
||||
ansible.builtin.set_fact:
|
||||
info: "{{ result.stdout_lines }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
@ -1,139 +0,0 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import GatheredAccount
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_org
|
||||
from users.models import User
|
||||
from .filter import GatherAccountsFilter
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
from ...notifications import GatherAccountChangeMsg
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_asset_mapper = {}
|
||||
self.asset_account_info = {}
|
||||
|
||||
self.asset_username_mapper = defaultdict(set)
|
||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.gather_accounts
|
||||
|
||||
def host_callback(self, host, asset=None, **kwargs):
|
||||
super().host_callback(host, asset=asset, **kwargs)
|
||||
self.host_asset_mapper[host['name']] = asset
|
||||
return host
|
||||
|
||||
def filter_success_result(self, tp, result):
|
||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||
return result
|
||||
|
||||
def generate_data(self, asset, result):
|
||||
data = []
|
||||
for username, info in result.items():
|
||||
self.asset_username_mapper[str(asset.id)].add(username)
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if info.get('date'):
|
||||
d['date_last_login'] = info['date']
|
||||
if info.get('address'):
|
||||
d['address_last_login'] = info['address'][:32]
|
||||
data.append(d)
|
||||
return data
|
||||
|
||||
def collect_asset_account_info(self, asset, result):
|
||||
data = self.generate_data(asset, result)
|
||||
self.asset_account_info[asset] = data
|
||||
|
||||
@staticmethod
|
||||
def get_nested_info(data, *keys):
|
||||
for key in keys:
|
||||
data = data.get(key, {})
|
||||
if not data:
|
||||
break
|
||||
return data
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
info = self.get_nested_info(result, 'debug', 'res', 'info')
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
result = self.filter_success_result(asset.type, info)
|
||||
self.collect_asset_account_info(asset, result)
|
||||
else:
|
||||
print(f'\033[31m Not found {host} info \033[0m\n')
|
||||
|
||||
def update_or_create_accounts(self):
|
||||
for asset, data in self.asset_account_info.items():
|
||||
with tmp_to_org(asset.org_id):
|
||||
gathered_accounts = []
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for d in data:
|
||||
username = d['username']
|
||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
||||
defaults=d, asset=asset, username=username,
|
||||
)
|
||||
gathered_accounts.append(gathered_account)
|
||||
if not self.is_sync_account:
|
||||
continue
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
super().run(*args, **kwargs)
|
||||
users, change_info = self.generate_send_users_and_change_info()
|
||||
self.update_or_create_accounts()
|
||||
self.send_email_if_need(users, change_info)
|
||||
|
||||
def generate_send_users_and_change_info(self):
|
||||
recipients = self.execution.recipients
|
||||
if not self.asset_username_mapper or not recipients:
|
||||
return None, None
|
||||
|
||||
users = User.objects.filter(id__in=recipients)
|
||||
if not users:
|
||||
return users, None
|
||||
|
||||
asset_ids = self.asset_username_mapper.keys()
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
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')))
|
||||
|
||||
system_asset_username_mapper = defaultdict(set)
|
||||
for asset_id, username in asset_id_username:
|
||||
system_asset_username_mapper[str(asset_id)].add(username)
|
||||
|
||||
change_info = {}
|
||||
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),
|
||||
}
|
||||
|
||||
return users, change_info
|
||||
|
||||
@staticmethod
|
||||
def send_email_if_need(users, change_info):
|
||||
if not users or not change_info:
|
||||
return
|
||||
|
||||
for user in users:
|
||||
GatherAccountChangeMsg(user, change_info).publish_async()
|
62
apps/accounts/automations/push_account/custom/ssh/main.yml
Normal file
62
apps/accounts/automations/push_account/custom/ssh/main.yml
Normal file
@ -0,0 +1,62 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
- name: Test privileged account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
register: ping_info
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Change asset password (paramiko)
|
||||
custom_command:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
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 }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
commands: "{{ params.commands }}"
|
||||
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
||||
ignore_errors: true
|
||||
when: ping_info is succeeded and check_conn_after_change
|
||||
register: change_info
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify password (paramiko)
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
delegate_to: localhost
|
||||
when: check_conn_after_change
|
@ -0,0 +1,32 @@
|
||||
id: push_account_by_ssh
|
||||
name: "{{ 'SSH account push' | trans }}"
|
||||
category:
|
||||
- device
|
||||
- host
|
||||
type:
|
||||
- all
|
||||
method: push_account
|
||||
protocol: ssh
|
||||
priority: 50
|
||||
params:
|
||||
- name: commands
|
||||
type: list
|
||||
label: "{{ 'Params commands label' | trans }}"
|
||||
default: [ '' ]
|
||||
help_text: "{{ 'Params commands help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
SSH account push:
|
||||
zh: '使用 SSH 命令行自定义推送'
|
||||
ja: 'SSHコマンドラインを使用してプッシュをカスタマイズする'
|
||||
en: 'Custom push using SSH command line'
|
||||
|
||||
Params commands help text:
|
||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
||||
|
||||
Params commands label:
|
||||
zh: '自定义命令'
|
||||
ja: 'カスタムコマンド'
|
||||
en: 'Custom command'
|
@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
@ -53,3 +53,4 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when: check_conn_after_change
|
||||
|
@ -1,9 +1,12 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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,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 }}"
|
||||
filter: version
|
||||
when: check_conn_after_change
|
||||
|
@ -1,7 +1,7 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
@ -40,3 +40,4 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
when: check_conn_after_change
|
||||
|
@ -1,7 +1,11 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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,8 +52,13 @@
|
||||
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
|
||||
- check_conn_after_change
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
@ -1,7 +1,7 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
@ -66,3 +66,4 @@
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT @@version
|
||||
when: check_conn_after_change
|
||||
|
@ -9,55 +9,20 @@
|
||||
database: passwd
|
||||
key: "{{ account.username }}"
|
||||
register: user_info
|
||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- 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: "{{ true if params.groups | length > 0 else false }}"
|
||||
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: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: user_info.msg is defined
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
@ -67,9 +32,61 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.msg is defined or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- 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: "{{ 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:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@ -79,14 +96,16 @@
|
||||
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('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@ -95,8 +114,10 @@
|
||||
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"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -5,6 +5,12 @@ type:
|
||||
- AIX
|
||||
method: push_account
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@ -28,12 +34,23 @@ 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)'
|
||||
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
|
||||
en: 'Using Ansible module user to push account (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@ -49,6 +66,16 @@ 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 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@ -59,3 +86,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
@ -9,55 +9,20 @@
|
||||
database: passwd
|
||||
key: "{{ account.username }}"
|
||||
register: user_info
|
||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- 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: "{{ true if params.groups | length > 0 else false }}"
|
||||
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: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: user_info.msg is defined
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
@ -67,9 +32,61 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.msg is defined or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- 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: "{{ 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:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@ -79,14 +96,16 @@
|
||||
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('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@ -95,8 +114,10 @@
|
||||
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"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -6,6 +6,12 @@ type:
|
||||
- linux
|
||||
method: push_account
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@ -30,12 +36,23 @@ 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)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
|
||||
en: 'Using Ansible module user to push account (sha512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@ -51,6 +68,16 @@ 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 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@ -59,4 +86,9 @@ i18n:
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
@ -4,10 +4,6 @@
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
- name: Push user password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
@ -28,4 +24,4 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user