mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 08:32:48 +00:00
Compare commits
1267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4def7bc5ec | ||
|
|
d401a44317 | ||
|
|
7e793a6e0a | ||
|
|
b61559d078 | ||
|
|
0e8260a37c | ||
|
|
b8af86b163 | ||
|
|
5000a1f586 | ||
|
|
a72bb192c3 | ||
|
|
225b315b7a | ||
|
|
084050a4cc | ||
|
|
2bdaf73d49 | ||
|
|
3052c4cc19 | ||
|
|
bd92045b50 | ||
|
|
23d867dce6 | ||
|
|
2c70e117c6 | ||
|
|
70e8a03888 | ||
|
|
d2df8acd84 | ||
|
|
6e5dcc738e | ||
|
|
d1ccb4af15 | ||
|
|
086ecfc046 | ||
|
|
8f3765a98d | ||
|
|
2d6610b133 | ||
|
|
5a82174c54 | ||
|
|
dac45f234a | ||
|
|
096b2cf8b8 | ||
|
|
0ed0f69be0 | ||
|
|
8af88cd2c6 | ||
|
|
2a9f0f8dcf | ||
|
|
cc2d47e6dc | ||
|
|
22c9dfc0f2 | ||
|
|
ed01f2f1fb | ||
|
|
c5ff0d972b | ||
|
|
7d9da9ff66 | ||
|
|
d6395b64fa | ||
|
|
9bfbdea508 | ||
|
|
cb1c906db4 | ||
|
|
12a0096963 | ||
|
|
a315e8888b | ||
|
|
454d3cba96 | ||
|
|
4a786baf4e | ||
|
|
187977c04a | ||
|
|
4260fe1424 | ||
|
|
7286b1b09e | ||
|
|
9a7919f3ac | ||
|
|
43cbf4f6a9 | ||
|
|
e9dc1ad86a | ||
|
|
90477146ed | ||
|
|
353b66bf8f | ||
|
|
3051996e35 | ||
|
|
8761ed741c | ||
|
|
0facd8a25e | ||
|
|
b28ce9de7a | ||
|
|
f2b72aae37 | ||
|
|
b001443f34 | ||
|
|
17303c0550 | ||
|
|
bac974b4f2 | ||
|
|
f9e970f4ed | ||
|
|
07c60ca75d | ||
|
|
bbe2678df3 | ||
|
|
d57f52ee24 | ||
|
|
55775f0deb | ||
|
|
fde118021e | ||
|
|
d484885762 | ||
|
|
8071f45f92 | ||
|
|
15cdd44c6c | ||
|
|
d44f90ea3d | ||
|
|
42e9fbf37a | ||
|
|
d44656aa10 | ||
|
|
09d4228182 | ||
|
|
3ae8da231a | ||
|
|
2e4b6d150a | ||
|
|
8542d53aff | ||
|
|
141dafc8bf | ||
|
|
28a6024b49 | ||
|
|
6cf2bc4baf | ||
|
|
631f802961 | ||
|
|
0eedda748a | ||
|
|
7183f0d274 | ||
|
|
c93ab15351 | ||
|
|
3fffd667dc | ||
|
|
11fd2afa3a | ||
|
|
1eb59b11da | ||
|
|
a1e2d4ca57 | ||
|
|
8a413563be | ||
|
|
203a01240b | ||
|
|
37fef6153a | ||
|
|
ba6c49e62b | ||
|
|
da79f8beab | ||
|
|
3f72c02049 | ||
|
|
380226a7d2 | ||
|
|
f88e5de3c1 | ||
|
|
7c149fe91b | ||
|
|
f673fed706 | ||
|
|
84326cc999 | ||
|
|
7d56678a8e | ||
|
|
25d1b71448 | ||
|
|
d3920a0cc9 | ||
|
|
52889cb67a | ||
|
|
0b67c7a953 | ||
|
|
1f50a2fe33 | ||
|
|
cd5094f10d | ||
|
|
c244cf5f43 | ||
|
|
a0db2f6ef8 | ||
|
|
ea485c3070 | ||
|
|
705f352cb9 | ||
|
|
dc13134b7b | ||
|
|
06de6c3575 | ||
|
|
c341d01e5a | ||
|
|
d1a3d31d3f | ||
|
|
10f4ff4eec | ||
|
|
25ea3ba01d | ||
|
|
072865f3e5 | ||
|
|
d5c9ec1c3d | ||
|
|
487c945d1d | ||
|
|
a78b2f4b62 | ||
|
|
a1f1dce56b | ||
|
|
a1221a39fd | ||
|
|
8c118b6f47 | ||
|
|
68fd8012d8 | ||
|
|
526928518d | ||
|
|
c5f6c564a7 | ||
|
|
076adec218 | ||
|
|
00d434ceea | ||
|
|
9acfd461b4 | ||
|
|
9424929dde | ||
|
|
b95d3ac9be | ||
|
|
af2a9bb1e6 | ||
|
|
63638ed1ce | ||
|
|
fa68389028 | ||
|
|
63b338085a | ||
|
|
5d8818e69e | ||
|
|
77521119b9 | ||
|
|
baee71e4b8 | ||
|
|
6459c20516 | ||
|
|
0207fe60c5 | ||
|
|
b8c43f5944 | ||
|
|
63ca2ab182 | ||
|
|
f9ea119928 | ||
|
|
d4bdc74bd8 | ||
|
|
42e7ca6a18 | ||
|
|
6e0341b7b1 | ||
|
|
2f25e2b24c | ||
|
|
87dcd2dcb7 | ||
|
|
e96aac058f | ||
|
|
d76bad125b | ||
|
|
913b1a1426 | ||
|
|
0213154a19 | ||
|
|
8f63b488b2 | ||
|
|
f8921004c2 | ||
|
|
5588eab57e | ||
|
|
e0a8f91741 | ||
|
|
467ebfa650 | ||
|
|
0533b77b1b | ||
|
|
a558ee2ac0 | ||
|
|
46a39701d4 | ||
|
|
8c3ab31e4e | ||
|
|
476e6cdc2f | ||
|
|
0b593f4555 | ||
|
|
456116938d | ||
|
|
76b24f62d4 | ||
|
|
e1eef0a3f3 | ||
|
|
2c74727b65 | ||
|
|
08cd91c426 | ||
|
|
90d269d2a2 | ||
|
|
a9ddbcc0cd | ||
|
|
aec31128cf | ||
|
|
b415ee051d | ||
|
|
082a5ae84c | ||
|
|
6b7554d69a | ||
|
|
5923562440 | ||
|
|
d144e7e572 | ||
|
|
cafbd08986 | ||
|
|
b436fc9b44 | ||
|
|
26fc56b4be | ||
|
|
3b1d199669 | ||
|
|
e7edbc9d84 | ||
|
|
41f81bc0bf | ||
|
|
75d2c81d33 | ||
|
|
da2dea5003 | ||
|
|
fc60156c23 | ||
|
|
76fb547551 | ||
|
|
e686f51703 | ||
|
|
7578bda588 | ||
|
|
f2d743ec2b | ||
|
|
7099aef360 | ||
|
|
246f5d8a11 | ||
|
|
6c98bd3b48 | ||
|
|
6def113cbd | ||
|
|
2dc0af2553 | ||
|
|
a291592e59 | ||
|
|
6fb4c1e181 | ||
|
|
eee093742c | ||
|
|
743c9bc3f1 | ||
|
|
f963c5ef9d | ||
|
|
2c46072db2 | ||
|
|
b375cd3e75 | ||
|
|
c26ca20ad8 | ||
|
|
982a510213 | ||
|
|
5d6880f6e9 | ||
|
|
a784a33203 | ||
|
|
a452f3307f | ||
|
|
b7a6287925 | ||
|
|
3cba8648cb | ||
|
|
ef7b2b7980 | ||
|
|
1ab247ac22 | ||
|
|
ef8a027849 | ||
|
|
7890e43f5a | ||
|
|
2030cbd19d | ||
|
|
0f7c8c2570 | ||
|
|
9b60d86ddd | ||
|
|
f129f99faa | ||
|
|
43f30b37da | ||
|
|
45aefa6b75 | ||
|
|
b30123054b | ||
|
|
b456e71ec4 | ||
|
|
7560b70c4d | ||
|
|
0c96df5283 | ||
|
|
9dda19b8d7 | ||
|
|
fbe5f9a63a | ||
|
|
1c4b4951dc | ||
|
|
8e12399058 | ||
|
|
741b96ddee | ||
|
|
8c3f89ee51 | ||
|
|
dee45ce2e0 | ||
|
|
cfab30f7f7 | ||
|
|
02e9a96792 | ||
|
|
aa6dcdc65d | ||
|
|
5a6b64eebd | ||
|
|
2dd7867b32 | ||
|
|
6507f0982c | ||
|
|
c115ef7b47 | ||
|
|
bf68ddf09e | ||
|
|
f3906ff998 | ||
|
|
e47ee43631 | ||
|
|
d22bb2c92f | ||
|
|
870dac37b9 | ||
|
|
d14c5c58ff | ||
|
|
a6b40510d0 | ||
|
|
f762fe73ff | ||
|
|
d49d1ba055 | ||
|
|
6e3d950e23 | ||
|
|
7939ef34b0 | ||
|
|
07cd930c0e | ||
|
|
c14c89a758 | ||
|
|
245367ec29 | ||
|
|
c5f4ecc8cc | ||
|
|
eb8bdf8623 | ||
|
|
a26c5a5e32 | ||
|
|
068db6d1ca | ||
|
|
e6e2a35745 | ||
|
|
fafc2791ab | ||
|
|
39507ef152 | ||
|
|
683fb9f596 | ||
|
|
ced9e53d62 | ||
|
|
93846234f8 | ||
|
|
8ac7d4b682 | ||
|
|
c4890f66e1 | ||
|
|
4618989813 | ||
|
|
29645768a0 | ||
|
|
8f1c934f73 | ||
|
|
7a45f4d129 | ||
|
|
55a5dd1e34 | ||
|
|
6695d0a8a2 | ||
|
|
84d6b3de26 | ||
|
|
17a5e919d5 | ||
|
|
3ba07867c8 | ||
|
|
75b76170f9 | ||
|
|
d34c7edb00 | ||
|
|
f64740c2db | ||
|
|
3a09845c29 | ||
|
|
09d51fd5be | ||
|
|
fc8181b5ed | ||
|
|
5a993c255d | ||
|
|
ad592fa504 | ||
|
|
1dcc8ff0a3 | ||
|
|
11a9a49bf8 | ||
|
|
b9ffc23066 | ||
|
|
ea4dccbab8 | ||
|
|
683461a49b | ||
|
|
1a1ad0f1a2 | ||
|
|
773f7048be | ||
|
|
f8f783745c | ||
|
|
4fe715d953 | ||
|
|
36dfc4bcb8 | ||
|
|
8925314dc7 | ||
|
|
817c02c667 | ||
|
|
58a10778cd | ||
|
|
fa81652de5 | ||
|
|
7e6fa27719 | ||
|
|
3e737c8cb8 | ||
|
|
345c0fcf4f | ||
|
|
bf6b685e8c | ||
|
|
654ec4970e | ||
|
|
4a436856b4 | ||
|
|
e993e7257c | ||
|
|
f12a59da2f | ||
|
|
42c3c85863 | ||
|
|
7e638ff8de | ||
|
|
932a65b840 | ||
|
|
81000953e2 | ||
|
|
dc742d1281 | ||
|
|
b1fceca8a6 | ||
|
|
d49d1e1414 | ||
|
|
dac3f7fc71 | ||
|
|
47989c41a3 | ||
|
|
ca34216141 | ||
|
|
905014d441 | ||
|
|
3e51f4d616 | ||
|
|
07179a4d22 | ||
|
|
7a2e93c087 | ||
|
|
3fb368c741 | ||
|
|
fca3a8fbca | ||
|
|
c1375ed7cb | ||
|
|
8b483b8c36 | ||
|
|
c465fccc33 | ||
|
|
3d934dc7c0 | ||
|
|
b69ed8cbe9 | ||
|
|
c27230762b | ||
|
|
7ea8205672 | ||
|
|
b9b55e3d67 | ||
|
|
900fc4420c | ||
|
|
0a3e5aed56 | ||
|
|
9fb6fd44d1 | ||
|
|
4214b220e1 | ||
|
|
ae80797ce4 | ||
|
|
d1be4a136e | ||
|
|
e8e211f47c | ||
|
|
44044a7d99 | ||
|
|
5854ad1975 | ||
|
|
0b1a1591f8 | ||
|
|
6241238b45 | ||
|
|
0f87f05b3f | ||
|
|
19c63a0b19 | ||
|
|
1fdc558ef7 | ||
|
|
9f6e26c4db | ||
|
|
628012a7ee | ||
|
|
c1579f5fe4 | ||
|
|
cbe0483b46 | ||
|
|
10c2935df4 | ||
|
|
10e06a4533 | ||
|
|
98e38ebfd8 | ||
|
|
9660e20176 | ||
|
|
21a7ec9fec | ||
|
|
7d123ff8c5 | ||
|
|
2af6ac504d | ||
|
|
6c8d1c4e77 | ||
|
|
52d3e1b34b | ||
|
|
bf6fcc9020 | ||
|
|
a0b756ebaa | ||
|
|
5e8a55f949 | ||
|
|
f9218584f4 | ||
|
|
228446979f | ||
|
|
aa37d86959 | ||
|
|
0e9079fa2e | ||
|
|
58c058c1a5 | ||
|
|
f390556a87 | ||
|
|
b7378da46e | ||
|
|
0c8c926aac | ||
|
|
81d8592ee1 | ||
|
|
af827f3626 | ||
|
|
91b269fc36 | ||
|
|
1605a57df6 | ||
|
|
5cd23b843a | ||
|
|
d46f1080f8 | ||
|
|
9a541ebf05 | ||
|
|
dba416f5eb | ||
|
|
7d7da9bf98 | ||
|
|
4425efd3c2 | ||
|
|
c6bb9e97fb | ||
|
|
9c7adb7a14 | ||
|
|
7b4faccf05 | ||
|
|
0cd3419e09 | ||
|
|
e49dedf6b1 | ||
|
|
bee4e05b5f | ||
|
|
a5419b49ee | ||
|
|
84e60283b8 | ||
|
|
96206384c0 | ||
|
|
78c61d5afa | ||
|
|
ee712d9a9d | ||
|
|
a1e8c2849a | ||
|
|
54751a715c | ||
|
|
a2907a6e6d | ||
|
|
33236aaa47 | ||
|
|
cd6c7ce7fa | ||
|
|
363baece4f | ||
|
|
1db0e28346 | ||
|
|
7366bbb197 | ||
|
|
7959f84bba | ||
|
|
0c96bf61ef | ||
|
|
39ce60c93a | ||
|
|
8ad78ffef8 | ||
|
|
66b499b8e3 | ||
|
|
22406f47f7 | ||
|
|
72f782b589 | ||
|
|
cf3df951a9 | ||
|
|
4085df913b | ||
|
|
d93f3aca51 | ||
|
|
b180a162cd | ||
|
|
1bf3ff5e1b | ||
|
|
0def477b63 | ||
|
|
337e1ba206 | ||
|
|
fe2d80046c | ||
|
|
f16a9ddb86 | ||
|
|
5f6c207721 | ||
|
|
988d686418 | ||
|
|
89e654af80 | ||
|
|
2ab1bbaa2c | ||
|
|
b43626b5a2 | ||
|
|
5e4b3e924f | ||
|
|
66b0173e20 | ||
|
|
67f6b1080e | ||
|
|
b56b897260 | ||
|
|
f031f4d560 | ||
|
|
d0e119fb50 | ||
|
|
7892e50aa2 | ||
|
|
bff3582136 | ||
|
|
bdf95903ce | ||
|
|
c1e6bc5d60 | ||
|
|
da588ce0ae | ||
|
|
d0680c3753 | ||
|
|
905d0d5131 | ||
|
|
d347ed9862 | ||
|
|
8611f765a3 | ||
|
|
962f1c0310 | ||
|
|
473a66719b | ||
|
|
aeb43a04f6 | ||
|
|
49a35985a1 | ||
|
|
21b789e08c | ||
|
|
51387ad97e | ||
|
|
290d584ac9 | ||
|
|
160b238058 | ||
|
|
938255df6f | ||
|
|
4230da0fd9 | ||
|
|
fee3715d30 | ||
|
|
689bd093be | ||
|
|
77461d7834 | ||
|
|
ee5894c296 | ||
|
|
07898004b0 | ||
|
|
630164cd51 | ||
|
|
981319e553 | ||
|
|
fedd32ea7a | ||
|
|
e57574f10a | ||
|
|
3f0a0b33b5 | ||
|
|
c21217d50c | ||
|
|
e44c8ae940 | ||
|
|
1da187c373 | ||
|
|
36ad42beb2 | ||
|
|
c0560ad3cc | ||
|
|
c318762f82 | ||
|
|
5d373c0137 | ||
|
|
3aea998bd2 | ||
|
|
c1ca48a32a | ||
|
|
2f0fcddc29 | ||
|
|
329565251a | ||
|
|
06a223376c | ||
|
|
47e8ad3aac | ||
|
|
c4fb3a8c04 | ||
|
|
9d4121c3b7 | ||
|
|
2eb1fe8547 | ||
|
|
e933774e6c | ||
|
|
0b994d1c46 | ||
|
|
381b150c2b | ||
|
|
53ebac9363 | ||
|
|
a0638dd5c4 | ||
|
|
5b741de896 | ||
|
|
d7f587216d | ||
|
|
019f00a34a | ||
|
|
9684b2d4ac | ||
|
|
2e190c9ea9 | ||
|
|
601a48071f | ||
|
|
bf885f94e4 | ||
|
|
7d4be819b8 | ||
|
|
26a7fa836c | ||
|
|
187329b006 | ||
|
|
8375008cfa | ||
|
|
16333fa1aa | ||
|
|
72deb005a6 | ||
|
|
18509a0ca4 | ||
|
|
e63d0dcd9e | ||
|
|
62ba3984bd | ||
|
|
db170aac9e | ||
|
|
5c7e73e2e0 | ||
|
|
f772296dff | ||
|
|
f6a26ac165 | ||
|
|
4e3b3442d2 | ||
|
|
2752770ce2 | ||
|
|
1840609d53 | ||
|
|
4f23090a5c | ||
|
|
898b51c593 | ||
|
|
2494418208 | ||
|
|
0fec70fe69 | ||
|
|
bcf90d71a2 | ||
|
|
f8f7ac0af5 | ||
|
|
d6c2705bd6 | ||
|
|
10f8b9f130 | ||
|
|
1e601288fa | ||
|
|
b1032761c8 | ||
|
|
c532c361c0 | ||
|
|
ec8dca90d6 | ||
|
|
a9f814a515 | ||
|
|
c4bbeaaccc | ||
|
|
0fd5ab02e9 | ||
|
|
745979074a | ||
|
|
8ae6863266 | ||
|
|
4fd7f0e949 | ||
|
|
732f0b55dc | ||
|
|
c0ec0f1343 | ||
|
|
aa6e550ba2 | ||
|
|
2ffaf59238 | ||
|
|
6c13fdbc46 | ||
|
|
35941ddf7f | ||
|
|
3ae976c183 | ||
|
|
999666f0eb | ||
|
|
1812074231 | ||
|
|
53eb32e620 | ||
|
|
50bd0b796d | ||
|
|
a02d80a2ae | ||
|
|
71a7eea8ad | ||
|
|
2b927caa60 | ||
|
|
053d958f9a | ||
|
|
8d25d0a653 | ||
|
|
62eb131f59 | ||
|
|
40eb7c79bb | ||
|
|
dabc9eb09b | ||
|
|
502657bad4 | ||
|
|
b5120e72c8 | ||
|
|
2ca659414e | ||
|
|
64f772e747 | ||
|
|
67a897f9c3 | ||
|
|
d0a9ccbdfe | ||
|
|
1a30675a86 | ||
|
|
f6273450bb | ||
|
|
8f35fcd6f9 | ||
|
|
1999cfdfeb | ||
|
|
c4af78c9f0 | ||
|
|
a3d02decd6 | ||
|
|
e623f63fcf | ||
|
|
4f1b2aceda | ||
|
|
94fc1fb53b | ||
|
|
937acbd0b5 | ||
|
|
067a70463e | ||
|
|
b115ed3b79 | ||
|
|
057fbdf0b1 | ||
|
|
5263a146e2 | ||
|
|
84070a558e | ||
|
|
e0604a3211 | ||
|
|
00e4c3cd07 | ||
|
|
97a0e27307 | ||
|
|
8d3c1bd783 | ||
|
|
db99ab80db | ||
|
|
1e8d9ba2ec | ||
|
|
7dddf0c3c2 | ||
|
|
891a5157a7 | ||
|
|
34b2a5fe0b | ||
|
|
de6908e5a6 | ||
|
|
d6527e3b02 | ||
|
|
33a29ae788 | ||
|
|
a2eb431015 | ||
|
|
8fbea2f702 | ||
|
|
af92271a52 | ||
|
|
391a5cb7d0 | ||
|
|
daf7d98f0e | ||
|
|
ed297fd1bd | ||
|
|
f91bef4105 | ||
|
|
a8d84fc6e1 | ||
|
|
0c7838d0e3 | ||
|
|
f26483c9cd | ||
|
|
5daca6592b | ||
|
|
0bced39f08 | ||
|
|
6d83dd0e3a | ||
|
|
46e99d10cb | ||
|
|
95eb11422a | ||
|
|
e8b3ee4565 | ||
|
|
1e99be1775 | ||
|
|
adae509bc0 | ||
|
|
7868e91844 | ||
|
|
a9bdbcf7c6 | ||
|
|
a809eac2b8 | ||
|
|
bdab93260f | ||
|
|
4ef3b2630a | ||
|
|
4eef25982d | ||
|
|
b82e9f860b | ||
|
|
6b46f5b48e | ||
|
|
fe717f0244 | ||
|
|
33fb063f78 | ||
|
|
7edc9c37f8 | ||
|
|
f8b4259a8c | ||
|
|
572d0e3f27 | ||
|
|
b334f3c2d9 | ||
|
|
6b4b9f4b02 | ||
|
|
d765e61991 | ||
|
|
9ccde03656 | ||
|
|
c66f366446 | ||
|
|
34d46897f8 | ||
|
|
2d9ce16601 | ||
|
|
0380be51dd | ||
|
|
47df0cfaab | ||
|
|
a2fb4a701e | ||
|
|
6e4381ac04 | ||
|
|
8ae03e4374 | ||
|
|
73f2022ff6 | ||
|
|
bc4258256a | ||
|
|
58dfe58ae0 | ||
|
|
53e3fa2590 | ||
|
|
23dbdaf6c0 | ||
|
|
3eba92548b | ||
|
|
ac5f2c560d | ||
|
|
f7f9331c48 | ||
|
|
77b4847bd9 | ||
|
|
0de9b29fa9 | ||
|
|
f9ca46dd67 | ||
|
|
ba28f3263d | ||
|
|
2e118665f5 | ||
|
|
bf53df46dc | ||
|
|
6449f36c7e | ||
|
|
ba35f5906b | ||
|
|
c8d7d42f66 | ||
|
|
20dacea260 | ||
|
|
d2dc2ab02c | ||
|
|
ba3b5a4027 | ||
|
|
3743761024 | ||
|
|
70055b8af2 | ||
|
|
726fd94f65 | ||
|
|
8b951ce12c | ||
|
|
189bc9d74a | ||
|
|
dd6c063478 | ||
|
|
5e9006d0c2 | ||
|
|
c42f69d1ba | ||
|
|
c7dfd0edce | ||
|
|
4382921c57 | ||
|
|
45feb468be | ||
|
|
c9b6b9a37a | ||
|
|
8010bdecea | ||
|
|
fc1c9c564a | ||
|
|
7c13b72739 | ||
|
|
6a4bc1f8b3 | ||
|
|
7d51d8c570 | ||
|
|
0ecd9fa32a | ||
|
|
b37c8b09bf | ||
|
|
23f22e92b8 | ||
|
|
c16319ec48 | ||
|
|
340547c889 | ||
|
|
54f5e65d36 | ||
|
|
4d6d4cbc22 | ||
|
|
7294f6e5e0 | ||
|
|
8ca2522c71 | ||
|
|
72f9d0d371 | ||
|
|
9a92e24e50 | ||
|
|
fea0170c5e | ||
|
|
5e5cd80bc2 | ||
|
|
e3511df4f8 | ||
|
|
11e5a97f14 | ||
|
|
4519ccfe1a | ||
|
|
657a2ac7e7 | ||
|
|
f5d8e125cb | ||
|
|
fd203c67c3 | ||
|
|
9fe5496ce9 | ||
|
|
c0875f6a87 | ||
|
|
d1a005f750 | ||
|
|
c52431b5ce | ||
|
|
4a9e83ba15 | ||
|
|
7712c1659e | ||
|
|
74c7b18dc4 | ||
|
|
5a3c67989b | ||
|
|
50918a3dd2 | ||
|
|
e9b174f342 | ||
|
|
63efbfe62e | ||
|
|
99cce185dd | ||
|
|
ab0fda93f6 | ||
|
|
d9552c0038 | ||
|
|
f0f493081a | ||
|
|
c4727e1eba | ||
|
|
ce8143c2ec | ||
|
|
65ad63272c | ||
|
|
4a4d5f3243 | ||
|
|
4563743f00 | ||
|
|
7b679f3e82 | ||
|
|
3d6aa15ece | ||
|
|
94a798eb01 | ||
|
|
ec393c1440 | ||
|
|
6571209864 | ||
|
|
d042de7b09 | ||
|
|
5e6e97c822 | ||
|
|
f146873501 | ||
|
|
35dfdf831a | ||
|
|
2b31cb2806 | ||
|
|
e43ffa7994 | ||
|
|
b0a9a83231 | ||
|
|
7da14571ac | ||
|
|
73b67da4c0 | ||
|
|
4bf2371cf0 | ||
|
|
075cbc497b | ||
|
|
1a0d9a20f9 | ||
|
|
fdb8416cac | ||
|
|
e2d5b69510 | ||
|
|
9944474ba0 | ||
|
|
ce6b9de07c | ||
|
|
b97759687d | ||
|
|
68b6236de2 | ||
|
|
6616374c30 | ||
|
|
682f6b2fb9 | ||
|
|
a2e3979916 | ||
|
|
f11d3c1cf2 | ||
|
|
f0bad5f107 | ||
|
|
ad3bc72dfb | ||
|
|
de9c69843d | ||
|
|
d2678e2a43 | ||
|
|
632ea87f07 | ||
|
|
4e7e1d5e15 | ||
|
|
1ac8537a34 | ||
|
|
dcaa798c2e | ||
|
|
8da4027e32 | ||
|
|
32e2d19553 | ||
|
|
48d1eecc08 | ||
|
|
0ab88ce754 | ||
|
|
bee5500425 | ||
|
|
7c03af7668 | ||
|
|
7a61a671a2 | ||
|
|
4a1fc0e2ac | ||
|
|
1e5e87e62a | ||
|
|
96c3b81383 | ||
|
|
297fedeffa | ||
|
|
9cd5675209 | ||
|
|
a5179d1596 | ||
|
|
c2463fe573 | ||
|
|
2f8042141c | ||
|
|
06a4e0d395 | ||
|
|
bb9d92fd7e | ||
|
|
749f9d3f81 | ||
|
|
03ad7777d0 | ||
|
|
7e4f20f443 | ||
|
|
607b7fd29f | ||
|
|
8895763ab4 | ||
|
|
8b1e202e68 | ||
|
|
32fe8f674c | ||
|
|
b4ef7bef55 | ||
|
|
31982c6547 | ||
|
|
67d3b63c6d | ||
|
|
f34fb5d9d5 | ||
|
|
3ec78ff9be | ||
|
|
f361621ab5 | ||
|
|
cd9587f68e | ||
|
|
2ff01a4bb3 | ||
|
|
06ed358fbc | ||
|
|
3e11249e8c | ||
|
|
6b5435b768 | ||
|
|
7d5a13de38 | ||
|
|
07bd44990b | ||
|
|
e4938ffc85 | ||
|
|
85d226eb07 | ||
|
|
c9a9ca7923 | ||
|
|
306f7a08d1 | ||
|
|
b86f9ac871 | ||
|
|
2562386fe0 | ||
|
|
61d4311e24 | ||
|
|
370e1628be | ||
|
|
adf5c4a7b9 | ||
|
|
9fc1ae7b6d | ||
|
|
313757dbe9 | ||
|
|
b32e352b24 | ||
|
|
b950b48112 | ||
|
|
519eb3bef2 | ||
|
|
4e55d0f1e4 | ||
|
|
2b3bb65114 | ||
|
|
b597cfcd19 | ||
|
|
33952b2333 | ||
|
|
a47a9c0345 | ||
|
|
4e0c056867 | ||
|
|
a9b5599db5 | ||
|
|
8a2eb70ad2 | ||
|
|
776234e8cc | ||
|
|
e2406955bc | ||
|
|
dba9550bc0 | ||
|
|
6ad1362a3f | ||
|
|
dfa2f7d6c9 | ||
|
|
c55e2db75e | ||
|
|
fd3a4d887e | ||
|
|
42afc1e0bf | ||
|
|
50c89431df | ||
|
|
f1f5017be3 | ||
|
|
9b85aafa52 | ||
|
|
817268d7cd | ||
|
|
d3bbfdc458 | ||
|
|
18a390d66a | ||
|
|
73b57a662e | ||
|
|
ea325f6e52 | ||
|
|
1216f15e45 | ||
|
|
cc3911d2f1 | ||
|
|
36c083f674 | ||
|
|
98c6a93658 | ||
|
|
adc607dafe | ||
|
|
1e85805ea3 | ||
|
|
957d3660ce | ||
|
|
049f6dca67 | ||
|
|
7f4377b0e8 | ||
|
|
7dfd0ee8fe | ||
|
|
41f375a4f7 | ||
|
|
a50dfe9c18 | ||
|
|
bd8a1a7d0e | ||
|
|
5546719712 | ||
|
|
068b39d922 | ||
|
|
2e1763cce7 | ||
|
|
ff9e470ce2 | ||
|
|
3080bf3647 | ||
|
|
0b04821794 | ||
|
|
296bb88834 | ||
|
|
c57cce8881 | ||
|
|
174cc16980 | ||
|
|
5b2649f775 | ||
|
|
83829df70c | ||
|
|
64641a18e6 | ||
|
|
09303ecc56 | ||
|
|
5f48e7aeb2 | ||
|
|
25dfce621b | ||
|
|
102d3b590b | ||
|
|
a45f581b0e | ||
|
|
b3991d0388 | ||
|
|
184e8b31e6 | ||
|
|
615bcadf62 | ||
|
|
7b2f813e7f | ||
|
|
81170b4b7b | ||
|
|
c4eacbabc6 | ||
|
|
ccb0509d85 | ||
|
|
886393c539 | ||
|
|
15b0ad9c12 | ||
|
|
19e2a5b9f9 | ||
|
|
0aa2c2016f | ||
|
|
935947c97a | ||
|
|
3e7e01418d | ||
|
|
7f42e59714 | ||
|
|
840e5e8863 | ||
|
|
24fb8b2a89 | ||
|
|
c1bf854824 | ||
|
|
ab23a357f7 | ||
|
|
78bf6f5817 | ||
|
|
91a26abf9e | ||
|
|
d7e7c62c7a | ||
|
|
09bdff4a67 | ||
|
|
56328e112a | ||
|
|
1d15f7125e | ||
|
|
e6b17da57d | ||
|
|
1870fc97d5 | ||
|
|
f548b4bd2b | ||
|
|
a56ac7b34e | ||
|
|
51c9a89b1f | ||
|
|
6f3ead3c42 | ||
|
|
1036d1c132 | ||
|
|
5de5fa2e96 | ||
|
|
19043d0a66 | ||
|
|
bc3e50a529 | ||
|
|
a7ab7da61c | ||
|
|
b483f78d52 | ||
|
|
88d8a3326f | ||
|
|
8f7dcd512a | ||
|
|
d795867916 | ||
|
|
4c4f544f0d | ||
|
|
8ec26dea43 | ||
|
|
799d1e4043 | ||
|
|
b03642847e | ||
|
|
a4e635bff0 | ||
|
|
83cc339d4b | ||
|
|
bb9790a50f | ||
|
|
9be3cbb936 | ||
|
|
e599bca951 | ||
|
|
501ad698b7 | ||
|
|
50e6c96358 | ||
|
|
7cf6e54f01 | ||
|
|
709e7af953 | ||
|
|
93474766f6 | ||
|
|
542eb25e7b | ||
|
|
609d2710fa | ||
|
|
d852d2f670 | ||
|
|
087a3f2914 | ||
|
|
36f113e307 | ||
|
|
23afe81ff5 | ||
|
|
dd5b2b9101 | ||
|
|
d363118911 | ||
|
|
351d4d8123 | ||
|
|
efb9f48c6f | ||
|
|
d04b90b8e8 | ||
|
|
66f57fdb27 | ||
|
|
c949589564 | ||
|
|
992708abe8 | ||
|
|
f63f8d085d | ||
|
|
3e55447327 | ||
|
|
3ac20d80d1 | ||
|
|
3e38e4fc59 | ||
|
|
1090887b2b | ||
|
|
7c55d462cd | ||
|
|
ea16088c08 | ||
|
|
4de9e608b1 | ||
|
|
dd9a55bd5f | ||
|
|
ee22006683 | ||
|
|
09d91d8bf3 | ||
|
|
46fbc19697 | ||
|
|
44a42e4739 | ||
|
|
4de2ae607d | ||
|
|
9fee82cd14 | ||
|
|
9126c7780d | ||
|
|
0842553f8a | ||
|
|
5fae919499 | ||
|
|
1243546627 | ||
|
|
a0cb16e5c4 | ||
|
|
7b8f932dcd | ||
|
|
243eedc4f9 | ||
|
|
230ef2f662 | ||
|
|
42019c9e8a | ||
|
|
f6622f5e01 | ||
|
|
31f098449f | ||
|
|
0d4e346210 | ||
|
|
df193162f7 | ||
|
|
646f0a568b | ||
|
|
be5b4a5f71 | ||
|
|
e61511372c | ||
|
|
d4f3280427 | ||
|
|
083f061665 | ||
|
|
be7a93d81a | ||
|
|
156be0a64e | ||
|
|
a7fa2331bd | ||
|
|
9e0d731a0c | ||
|
|
1b184db956 | ||
|
|
4b9ed47cda | ||
|
|
f04e2fa090 | ||
|
|
83d12d02fb | ||
|
|
64257823c5 | ||
|
|
a7468a243d | ||
|
|
528e251f31 | ||
|
|
86a055638c | ||
|
|
b3f359d47b | ||
|
|
dbe969b064 | ||
|
|
b9258878fe | ||
|
|
19c2973501 | ||
|
|
e7a3c5a822 | ||
|
|
ff4748f9f4 | ||
|
|
60c19148dc | ||
|
|
7eedc0635e | ||
|
|
f5fd40978e | ||
|
|
72dd23dcce | ||
|
|
5b5c33116a | ||
|
|
7167515a53 | ||
|
|
17a01a12db | ||
|
|
3188692691 | ||
|
|
aab59403e1 | ||
|
|
7e7e24f51f | ||
|
|
428e8bf2a0 | ||
|
|
24b1c87121 | ||
|
|
cef93abb2f | ||
|
|
5c483084b7 | ||
|
|
167734ca5d | ||
|
|
1a9a5c28f5 | ||
|
|
430e20a49c | ||
|
|
3b056ff953 | ||
|
|
795d1b59e0 | ||
|
|
9d4f1a01fd | ||
|
|
332f65cf2f | ||
|
|
b79e6799c4 | ||
|
|
4eef425e2a | ||
|
|
3f4877f26b | ||
|
|
0e4d778335 | ||
|
|
52d20080ff | ||
|
|
ed8d72c06b | ||
|
|
5e9e3ec6f6 | ||
|
|
4f5f92deb8 | ||
|
|
d2a15ee702 | ||
|
|
a3a591da4b | ||
|
|
3f2925116e | ||
|
|
c3e2e536e0 | ||
|
|
8c133d5fdb | ||
|
|
89d8efe0f1 | ||
|
|
54303ea33f | ||
|
|
4dcd8dd8dd | ||
|
|
4f04a7d258 | ||
|
|
bf308e24b6 | ||
|
|
b3642f3ff4 | ||
|
|
3aed4955c8 | ||
|
|
9a5f9a9c92 | ||
|
|
6d5bec1ef2 | ||
|
|
e93fd1fd44 | ||
|
|
7bf37611bd | ||
|
|
b8ec4bfaa5 | ||
|
|
58b6293b76 | ||
|
|
8e12eebceb | ||
|
|
72d6ea43fa | ||
|
|
deedd49dc5 | ||
|
|
a36e6fbf84 | ||
|
|
b57453cc3c | ||
|
|
62f2909d59 | ||
|
|
0d469ff95b | ||
|
|
ca883f1fb4 | ||
|
|
6e0fbd78e7 | ||
|
|
0813cff74f | ||
|
|
ff428b84f9 | ||
|
|
d0c9aa2c55 | ||
|
|
1d5e603c0d | ||
|
|
ddbbc8df17 | ||
|
|
90df404931 | ||
|
|
b9cbff1a5f | ||
|
|
b9717eece3 | ||
|
|
f9cf2a243b | ||
|
|
e056430fce | ||
|
|
2b2821c0a1 | ||
|
|
213221beae | ||
|
|
2db9c90a74 | ||
|
|
8ced6f1168 | ||
|
|
6703ab9a77 | ||
|
|
2fc6e6cd54 | ||
|
|
2176fd8fac | ||
|
|
856e7c16e5 | ||
|
|
d4feaf1e08 | ||
|
|
5aee2ce3db | ||
|
|
4424c4bde2 | ||
|
|
5863e3e008 | ||
|
|
79a371eb6c | ||
|
|
7c7de96158 | ||
|
|
80b03e73f6 | ||
|
|
32dbab2e34 | ||
|
|
b189e363cc | ||
|
|
4c3a655239 | ||
|
|
5533114db5 | ||
|
|
4c469afa95 | ||
|
|
2ccc5beeda | ||
|
|
4b67d6925e | ||
|
|
dd979f582a | ||
|
|
042ea5e137 | ||
|
|
2a6f68c7ba | ||
|
|
43b5e97b95 | ||
|
|
619b521ea1 | ||
|
|
3447eeda68 | ||
|
|
75ef413ea5 | ||
|
|
662c9092dc | ||
|
|
c8d54b28e2 | ||
|
|
96cd307d1f | ||
|
|
6385cb3f86 | ||
|
|
36e9d8101a | ||
|
|
3354ab8ce9 | ||
|
|
89ec6ba6ef | ||
|
|
af40e46a75 | ||
|
|
86fcd3c251 | ||
|
|
c2d5928273 | ||
|
|
e656ba70ec | ||
|
|
bb807e6251 | ||
|
|
bbd6cae3d7 | ||
|
|
c3b09dd800 | ||
|
|
6d427b9834 | ||
|
|
610aaf5244 | ||
|
|
df2f1b3e6e | ||
|
|
f26b7a470a | ||
|
|
a4667f3312 | ||
|
|
91081d9423 | ||
|
|
3041697edc | ||
|
|
75d7530ea5 | ||
|
|
975cc41bce | ||
|
|
439999381d | ||
|
|
39ab5978be | ||
|
|
7be7c8cee1 | ||
|
|
68b22cbdec | ||
|
|
a7c704bea3 | ||
|
|
21993b0d89 | ||
|
|
73ccf3be5f | ||
|
|
bf3056abc4 | ||
|
|
f2fd9f5990 | ||
|
|
6d39a51c36 | ||
|
|
7fa94008c9 | ||
|
|
9685a25dc6 | ||
|
|
1af4fcd381 | ||
|
|
177055acdc | ||
|
|
6ec0b3ad54 | ||
|
|
49dd611292 | ||
|
|
f557c1dace | ||
|
|
6e87b94789 | ||
|
|
b0dba35e5a | ||
|
|
d0b19f20c3 | ||
|
|
3e78d627f8 | ||
|
|
0763404235 | ||
|
|
31cd441a34 | ||
|
|
40c0aac5a9 | ||
|
|
83099dcd16 | ||
|
|
0db72bd00f | ||
|
|
732b8cc0b8 | ||
|
|
a9f90b4e31 | ||
|
|
cf1fbabca1 | ||
|
|
cbcefe8bd3 | ||
|
|
133a2e4714 | ||
|
|
b4b9149d5d | ||
|
|
9af4d5f76f | ||
|
|
96d26cc96f | ||
|
|
18d8f59bb5 | ||
|
|
841f707b6d | ||
|
|
448c5db3bb | ||
|
|
75b886675e | ||
|
|
24e22115de | ||
|
|
b18ead0ffa | ||
|
|
6e8922da1c | ||
|
|
dcb38ef534 | ||
|
|
8a693d9fa7 | ||
|
|
487932590b | ||
|
|
79b5aa68c8 | ||
|
|
50a4735b07 | ||
|
|
1183109354 | ||
|
|
202e619c4b | ||
|
|
179cb7531c | ||
|
|
987f840431 | ||
|
|
f04544e8df | ||
|
|
cd6dc6a722 | ||
|
|
150552d734 | ||
|
|
388314ca5a | ||
|
|
26d00329e7 | ||
|
|
e93be8f828 | ||
|
|
2690092faf | ||
|
|
eabaae81ac | ||
|
|
6df331cbed | ||
|
|
0390e37fd5 | ||
|
|
44d9aff573 | ||
|
|
2b4f8bd11c | ||
|
|
231332585d | ||
|
|
531de188d6 | ||
|
|
0c1f717fb2 | ||
|
|
9d9177ed05 | ||
|
|
ab77d5db4b | ||
|
|
eadecb83ed | ||
|
|
5d6088abd3 | ||
|
|
38f7c123e5 | ||
|
|
d7daf7071a | ||
|
|
795245d7f4 | ||
|
|
7ea2a0d6a5 | ||
|
|
c90b9d70dc | ||
|
|
f6c24f809c | ||
|
|
e369a8d51f | ||
|
|
c74c9f51f0 | ||
|
|
57bf9ca8b1 | ||
|
|
ddc2d1106b | ||
|
|
15992c636a | ||
|
|
36cd18ab9a | ||
|
|
676ee93837 | ||
|
|
c02f8e499b | ||
|
|
4ebb4d1b6d | ||
|
|
5e7650d719 | ||
|
|
bf302f47e5 | ||
|
|
1ddc228449 | ||
|
|
c9065fd96e | ||
|
|
4a09dc6e3e | ||
|
|
55bfb942e2 | ||
|
|
9aed51ffe9 | ||
|
|
a98816462f | ||
|
|
abe32e6c79 | ||
|
|
77c8ca5863 | ||
|
|
8fa15b3378 | ||
|
|
a3507975fb | ||
|
|
76ca6d587d | ||
|
|
038582a8c1 | ||
|
|
ca2fc3cb5e | ||
|
|
cc30b766f8 | ||
|
|
b7bd88b8a0 | ||
|
|
5518e1e00f | ||
|
|
0632e88f5d | ||
|
|
9dc2255894 | ||
|
|
1baf35004d | ||
|
|
5acff310f7 | ||
|
|
fdded8b90f | ||
|
|
1d550cbe64 | ||
|
|
4847b7a680 | ||
|
|
1c551b4fe8 | ||
|
|
6ffba739f2 | ||
|
|
0282346945 | ||
|
|
f6d9af8beb | ||
|
|
ba4e6e9a9f | ||
|
|
874a3eeebf | ||
|
|
dd793a4eca | ||
|
|
f7e6c14bc5 | ||
|
|
f6031d6f5d | ||
|
|
5e779e6542 | ||
|
|
7031b7f28b | ||
|
|
e2f540a1f4 | ||
|
|
108a1da212 | ||
|
|
b4a8cb768b | ||
|
|
6b2f606430 | ||
|
|
70a8db895d | ||
|
|
0043dc6110 | ||
|
|
87d2798612 | ||
|
|
e2d8eee629 | ||
|
|
8404db8cef | ||
|
|
fd7f379b10 | ||
|
|
111c63ee6a | ||
|
|
4eb5d51840 | ||
|
|
7f53a80855 | ||
|
|
90afabdcb2 | ||
|
|
de405be753 | ||
|
|
f84b845385 | ||
|
|
b1ac3fa94f | ||
|
|
32fab08ed3 | ||
|
|
8943850ca9 | ||
|
|
8fff57813a | ||
|
|
9128210e87 | ||
|
|
e3dd03f4c7 | ||
|
|
aabb2aff1f | ||
|
|
f8bbca38e3 | ||
|
|
12b180ddea | ||
|
|
4917769964 | ||
|
|
5868d56e18 | ||
|
|
bab76f3cda | ||
|
|
475c0e4187 | ||
|
|
3d070231f4 | ||
|
|
c5b0cafabd | ||
|
|
a69bba8702 | ||
|
|
cfd0098019 | ||
|
|
52f1dcf662 | ||
|
|
373c6c77e0 | ||
|
|
f3d052554d | ||
|
|
a57ce482dd | ||
|
|
a449d97f67 | ||
|
|
84e4238848 | ||
|
|
82dd1c35ea | ||
|
|
5ac974a44c | ||
|
|
c4caeb92ee | ||
|
|
f4799d90c0 | ||
|
|
f97685c788 | ||
|
|
0439376326 | ||
|
|
dd2413edd8 | ||
|
|
459c5c07c9 | ||
|
|
ef86a49c1e | ||
|
|
0ad389515b | ||
|
|
2432b9a553 | ||
|
|
12216a718a | ||
|
|
2190db1bb5 | ||
|
|
5d36537404 | ||
|
|
5a87634c26 | ||
|
|
e5eb84999a | ||
|
|
426a86b52d | ||
|
|
f84acfe282 | ||
|
|
c73b49fe30 | ||
|
|
98238f71ae | ||
|
|
66e45f1c80 | ||
|
|
93a400f6e6 | ||
|
|
535d7d8373 | ||
|
|
db268280b4 | ||
|
|
873789bdab | ||
|
|
6584890ab1 | ||
|
|
96d5c519ec | ||
|
|
6e91217303 | ||
|
|
5dd1dfc59e | ||
|
|
a53e930950 | ||
|
|
8f52f79d91 | ||
|
|
3af0e68c84 | ||
|
|
3ccf32ed48 | ||
|
|
d52ed2ffb9 | ||
|
|
38588151d1 | ||
|
|
2a95aca28f | ||
|
|
1915224063 | ||
|
|
da4f9efb42 | ||
|
|
579c2c1d7a | ||
|
|
2a86c3a376 | ||
|
|
5558e854de | ||
|
|
31b6c3b679 | ||
|
|
2209a2c8d2 | ||
|
|
f596b65ed7 | ||
|
|
2c9c64a13f | ||
|
|
1d49b3deca | ||
|
|
6701a1b604 | ||
|
|
b8ff3b38bf | ||
|
|
d3be16ffe8 | ||
|
|
e3648d11b1 | ||
|
|
d4037998c8 | ||
|
|
82de636b5c | ||
|
|
91f1280f97 | ||
|
|
7c82f5aa2b | ||
|
|
6a801eaf33 | ||
|
|
28da819735 |
@@ -6,4 +6,5 @@ tmp/*
|
||||
django.db
|
||||
celerybeat.pid
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
20
.github/ISSUE_TEMPLATE.md
vendored
20
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,20 +0,0 @@
|
||||
[简述你的问题]
|
||||
|
||||
|
||||
##### 使用版本
|
||||
[请提供你使用的JumpServer版本 如 2.0.1 注: 1.4及以下版本不再提供支持]
|
||||
|
||||
##### 使用浏览器版本
|
||||
[请提供你使用的浏览器版本 如 Chrome 84.0.4147.105 ]
|
||||
|
||||
##### 问题复现步骤
|
||||
1. [步骤1]
|
||||
2. [步骤2]
|
||||
|
||||
##### 具体表现[截图可能会更好些,最好能截全]
|
||||
|
||||
|
||||
##### 其他
|
||||
|
||||
|
||||
[注:] 完成后请关闭 issue
|
||||
10
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 提出针对本项目的想法和建议
|
||||
title: "[Feature] "
|
||||
labels: 类型:需求
|
||||
assignees: ibuler
|
||||
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug 提交
|
||||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: 类型:bug
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**JumpServer 版本(v1.5.9以下不再支持)**
|
||||
|
||||
|
||||
**浏览器版本**
|
||||
|
||||
|
||||
**Bug 描述**
|
||||
|
||||
|
||||
**Bug 重现步骤(有截图更好)**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: 问题咨询
|
||||
about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 类型:提问
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**请描述您的问题.**
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,6 +15,7 @@ dump.rdb
|
||||
.tox
|
||||
.cache/
|
||||
.idea/
|
||||
.vscode/
|
||||
db.sqlite3
|
||||
config.py
|
||||
config.yml
|
||||
@@ -38,3 +39,4 @@ logs/*
|
||||
.vagrant/
|
||||
release/*
|
||||
releashe
|
||||
/apps/script.py
|
||||
|
||||
50
Dockerfile
50
Dockerfile
@@ -1,5 +1,6 @@
|
||||
FROM registry.fit2cloud.com/public/python:v3 as stage-build
|
||||
MAINTAINER Jumpserver Team <ibuler@qq.com>
|
||||
# 编译代码
|
||||
FROM python:3.8.6-slim as stage-build
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
@@ -7,33 +8,50 @@ WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
|
||||
FROM registry.fit2cloud.com/public/python:v3
|
||||
# 构建运行时环境
|
||||
FROM python:3.8.6-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
|
||||
ENV MYSQL_MIRROR=$MYSQL_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
COPY ./requirements ./requirements
|
||||
RUN useradd jumpserver
|
||||
RUN yum -y install epel-release && \
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
RUN yum -y install $(cat requirements/rpm_requirements.txt)
|
||||
RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \
|
||||
pip config set global.index-url ${PIP_MIRROR}
|
||||
RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt
|
||||
COPY ./requirements/deb_requirements.txt ./requirements/deb_requirements.txt
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& apt update \
|
||||
&& apt -y install telnet iproute2 redis-tools default-mysql-client vim wget curl locales procps \
|
||||
&& apt -y install $(cat requirements/deb_requirements.txt) \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc
|
||||
|
||||
COPY ./requirements/requirements.txt ./requirements/requirements.txt
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
RUN mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
RUN mkdir -p /opt/jumpserver/oracle/ \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar > /dev/null \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
|
||||
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
|
||||
|
||||
RUN echo > config.yml
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
ENV LC_ALL=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8070
|
||||
EXPOSE 8080
|
||||
|
||||
268
README.md
268
README.md
@@ -1,222 +1,107 @@
|
||||
# JumpServer 多云环境下更好用的堡垒机
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0"><img src="https://shields.io/github/license/jumpserver/jumpserver" alt="License: GPL v2"></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>
|
||||
|
||||
--------------------------
|
||||
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
|
||||
|Developer Wanted|
|
||||
|------------------|
|
||||
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
改变世界,从一点点开始。
|
||||
改变世界,从一点点开始 ...
|
||||
|
||||
> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
|
||||
> 如需进一步了解 JumpServer 开源项目,推荐阅读 [JumpServer 的初心和使命](https://mp.weixin.qq.com/s/S6q_2rP_9MwaVwyqLQnXzA)
|
||||
|
||||
## 特色优势
|
||||
### 特色优势
|
||||
|
||||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用。
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
## 版本说明
|
||||
### UI 展示
|
||||
|
||||
自 v2.0.0 发布后, JumpServer 版本号命名将变更为:v大版本.功能版本.Bug修复版本。比如:
|
||||

|
||||
|
||||
```
|
||||
v2.0.1 是 v2.0.0 之后的Bug修复版本;
|
||||
v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
```
|
||||
### 在线体验
|
||||
|
||||
像其它优秀开源项目一样,JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如:
|
||||
- 环境地址:<https://demo.jumpserver.org/>
|
||||
|
||||
```
|
||||
在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3;
|
||||
在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4;v2.1 会停止维护。
|
||||
```
|
||||
| :warning: 注意 |
|
||||
| :--------------------------- |
|
||||
| 该环境仅作体验目的使用,我们会定时清理、重置数据! |
|
||||
| 请勿修改体验环境用户的密码! |
|
||||
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
|
||||
|
||||
## 功能列表
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="8">身份认证<br>Authentication</td>
|
||||
<td rowspan="5">登录认证</td>
|
||||
<td>资源统一登录与认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LDAP/AD 认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS 认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OpenID 认证(实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAS 认证 (实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">MFA认证</td>
|
||||
<td>MFA 二次认证(Google Authenticator)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS 二次认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>登录复核(X-PACK)</td>
|
||||
<td>用户登录行为受管理员的监管与控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="11">账号管理<br>Account</td>
|
||||
<td rowspan="2">集中账号</td>
|
||||
<td>管理用户管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>系统用户管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">统一密码</td>
|
||||
<td>资产密码托管</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动生成密码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动推送密码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码过期设置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">批量改密(X-PACK)</td>
|
||||
<td>定期批量改密</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多种密码策略</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多云纳管(X-PACK)</td>
|
||||
<td>对私有云、公有云资产自动统一纳管</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>收集用户(X-PACK)</td>
|
||||
<td>自定义任务定期收集主机用户</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码匣子(X-PACK)</td>
|
||||
<td>统一对资产主机的用户密码进行查看、更新、测试操作</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="15">授权控制<br>Authorization</td>
|
||||
<td>多维授权</td>
|
||||
<td>对用户、用户组、资产、资产节点、应用以及系统用户进行授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">资产授权</td>
|
||||
<td>资产以树状结构进行展示</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>资产和节点均可灵活授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>节点内资产自动继承授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>子节点自动继承父节点授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">应用授权</td>
|
||||
<td>实现更细粒度的应用级授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MySQL 数据库应用、RemoteApp 远程应用(X-PACK)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>动作授权</td>
|
||||
<td>实现对授权资产的文件上传、下载以及连接动作的控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>时间授权</td>
|
||||
<td>实现对授权资源使用时间段的限制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>特权指令</td>
|
||||
<td>实现对特权指令的使用(支持黑白名单)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>命令过滤</td>
|
||||
<td>实现对授权系统用户所执行的命令进行控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输</td>
|
||||
<td>SFTP 文件上传/下载</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件管理</td>
|
||||
<td>实现 Web SFTP 文件管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>工单管理(X-PACK)</td>
|
||||
<td>支持对用户登录请求行为进行控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>组织管理(X-PACK)</td>
|
||||
<td>实现多租户管理与权限隔离</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="7">安全审计<br>Audit</td>
|
||||
<td>操作审计</td>
|
||||
<td>用户操作行为审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">会话审计</td>
|
||||
<td>在线会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>历史会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">录像审计</td>
|
||||
<td>支持对 Linux、Windows 等资产操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持对 RemoteApp(X-PACK)、MySQL 等应用操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>指令审计</td>
|
||||
<td>支持对资产和应用等操作的命令进行审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输</td>
|
||||
<td>可对文件的上传、下载记录进行审计</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 快速开始
|
||||
### 快速开始
|
||||
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
- [完整文档](https://docs.jumpserver.org)
|
||||
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4)
|
||||
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
|
||||
## 组件项目
|
||||
### 组件项目
|
||||
- [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 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目
|
||||
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目
|
||||
|
||||
## JumpServer 企业版
|
||||
### 社区
|
||||
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose) 或加入到我们的社区当中进行进一步交流沟通。
|
||||
|
||||
#### 微信交流群
|
||||
|
||||
<img src="https://download.jumpserver.org/images/wecom-group.jpeg" alt="微信群二维码" width="200"/>
|
||||
|
||||
### 贡献
|
||||
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
|
||||
|
||||
感谢以下贡献者,让 JumpServer 更加完善
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
### 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
|
||||
|
||||
### JumpServer 企业版
|
||||
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
|
||||
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
|
||||
|
||||
## 案例研究
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
@@ -227,7 +112,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
|
||||
## 安全说明
|
||||
### 安全说明
|
||||
|
||||
JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
|
||||
|
||||
@@ -237,12 +122,13 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
## License & Copyright
|
||||
### License & Copyright
|
||||
|
||||
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
|
||||
Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (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-2.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.
|
||||
|
||||
|
||||
94
README_EN.md
94
README_EN.md
@@ -1,57 +1,91 @@
|
||||
## Jumpserver
|
||||
<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>
|
||||
|
||||

|
||||

|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](http://www.paramiko.org/)
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0"><img src="https://shields.io/github/license/jumpserver/jumpserver" alt="License: GPL v2"></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 GNU GPL v2.0. 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
|
||||
|
||||
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
- 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 is the first fully open source bastion in the world, based on the GNU GPL v2.0 open source protocol. Jumpserver is a professional operation and maintenance audit system conforms to 4A specifications.
|
||||
|
||||
Jumpserver is developed using Python / Django, conforms to the Web 2.0 specification, and is equipped with the industry-leading Web Terminal solution which have beautiful interface and great user experience.
|
||||
### 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/)
|
||||
|
||||
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
|
||||
### Contribution
|
||||
If you have any good ideas or helping us to fix bugs, please submit a Pull Request and accept our thanks :)
|
||||
|
||||
Change the world, starting from little things.
|
||||
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>
|
||||
|
||||
### Features
|
||||
<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>
|
||||
|
||||
### Start
|
||||
<a href="https://github.com/jumpserver/luna/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
|
||||
</a>
|
||||
|
||||
Quick start [Docker Install](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
### 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.
|
||||
|
||||
Step by Step deployment. [Docs](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
|
||||
Full documentation [Docs](http://docs.jumpserver.org)
|
||||
### JumpServer Enterprise Version
|
||||
- [Apply for it](https://jinshuju.net/f/kyOYpi)
|
||||
|
||||
### Demo、Video 和 Snapshot
|
||||
### Case Study
|
||||
|
||||
We provide online demo, demo video and screenshots to get you started quickly.
|
||||
- [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)。
|
||||
|
||||
[Demo](https://demo.jumpserver.org/auth/login/?next=/)
|
||||
[Video](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
|
||||
[Snapshot](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
### For safety instructions
|
||||
|
||||
### SDK
|
||||
JumpServer is a security product. Please refer to [Basic Security Recommendations](https://docs.jumpserver.org/zh/master/install/install_security/) for deployment and installation.
|
||||
|
||||
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
|
||||
|
||||
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
|
||||
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK thanks to 恺珺 for provide Java SDK
|
||||
If you find a security problem, please contact us directly:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.
|
||||
Copyright (c) 2014-2021 Beijing Duizhan Tech, Inc., All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 安全说明
|
||||
|
||||
JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
|
||||
|
||||
如果你发现安全问题,请直接联系我们,我们携手让世界更好:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
2
apps/.gitattributes
vendored
Normal file
2
apps/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.js linguist-language=python
|
||||
*.html linguist-language=python
|
||||
0
apps/acls/__init__.py
Normal file
0
apps/acls/__init__.py
Normal file
3
apps/acls/admin.py
Normal file
3
apps/acls/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
3
apps/acls/api/__init__.py
Normal file
3
apps/acls/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .login_asset_check import *
|
||||
20
apps/acls/api/login_acl.py
Normal file
20
apps/acls/api/login_acl.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from ..models import LoginACL
|
||||
from .. import serializers
|
||||
from ..filters import LoginAclFilter
|
||||
|
||||
__all__ = ['LoginACLViewSet', ]
|
||||
|
||||
|
||||
class LoginACLViewSet(JMSBulkModelViewSet):
|
||||
queryset = LoginACL.objects.all()
|
||||
filterset_class = LoginAclFilter
|
||||
search_fields = ('name',)
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LoginACLSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["retrieve", "list"]:
|
||||
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
|
||||
return super().get_permissions()
|
||||
14
apps/acls/api/login_asset_acl.py
Normal file
14
apps/acls/api/login_asset_acl.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
__all__ = ['LoginAssetACLViewSet']
|
||||
|
||||
|
||||
class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
||||
model = models.LoginAssetACL
|
||||
filterset_fields = ('name', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginAssetACLSerializer
|
||||
75
apps/acls/api/login_asset_check.py
Normal file
75
apps/acls/api/login_asset_check.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from common.permissions import IsAppUser
|
||||
from common.utils import reverse, lazyproperty
|
||||
from orgs.utils import tmp_to_org
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..models import LoginAssetACL
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
|
||||
|
||||
|
||||
class LoginAssetCheckAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.LoginAssetCheckSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
||||
return Response(data=response_data, status=200)
|
||||
|
||||
def check_if_need_confirm(self):
|
||||
queries = {
|
||||
'user': self.serializer.user, 'asset': self.serializer.asset,
|
||||
'system_user': self.serializer.system_user,
|
||||
'action': LoginAssetACL.ActionChoices.login_confirm
|
||||
}
|
||||
with tmp_to_org(self.serializer.org):
|
||||
acl = LoginAssetACL.filter(**queries).valid().first()
|
||||
|
||||
if not acl:
|
||||
is_need_confirm = False
|
||||
response_data = {}
|
||||
else:
|
||||
is_need_confirm = True
|
||||
response_data = self._get_response_data_of_need_confirm(acl)
|
||||
response_data['need_confirm'] = is_need_confirm
|
||||
return is_need_confirm, response_data
|
||||
|
||||
def _get_response_data_of_need_confirm(self, acl):
|
||||
ticket = LoginAssetACL.create_login_asset_confirm_ticket(
|
||||
user=self.serializer.user,
|
||||
asset=self.serializer.asset,
|
||||
system_user=self.serializer.system_user,
|
||||
assignees=acl.reviewers.all(),
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-acls:login-asset-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
view_name='api-tickets:ticket-detail',
|
||||
kwargs={'pk': str(ticket.id)},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
data = {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
'ticket_detail_url': ticket_detail_url,
|
||||
'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees]
|
||||
}
|
||||
return data
|
||||
|
||||
@lazyproperty
|
||||
def serializer(self):
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
5
apps/acls/apps.py
Normal file
5
apps/acls/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AclsConfig(AppConfig):
|
||||
name = 'acls'
|
||||
15
apps/acls/filters.py
Normal file
15
apps/acls/filters.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django_filters import rest_framework as filters
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
from acls.models import LoginACL
|
||||
|
||||
|
||||
class LoginAclFilter(BaseFilterSet):
|
||||
user = filters.UUIDFilter(field_name='user_id')
|
||||
user_display = filters.CharFilter(field_name='user__name')
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields = (
|
||||
'name', 'user', 'user_display', 'action'
|
||||
)
|
||||
61
apps/acls/migrations/0001_initial.py
Normal file
61
apps/acls/migrations/0001_initial.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 3.1 on 2021-03-11 09:53
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LoginACL',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('ip_group', models.JSONField(default=list, verbose_name='Login IP')),
|
||||
('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', '-date_updated', 'name'),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LoginAssetACL',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('users', models.JSONField(verbose_name='User')),
|
||||
('system_users', models.JSONField(verbose_name='System User')),
|
||||
('assets', models.JSONField(verbose_name='Asset')),
|
||||
('action', models.CharField(choices=[('login_confirm', 'Login confirm')], default='login_confirm', max_length=64, verbose_name='Action')),
|
||||
('reviewers', models.ManyToManyField(blank=True, related_name='review_login_asset_acls', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', '-date_updated', 'name'),
|
||||
'unique_together': {('name', 'org_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
98
apps/acls/migrations/0002_auto_20210926_1047.py
Normal file
98
apps/acls/migrations/0002_auto_20210926_1047.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Generated by Django 3.1.12 on 2021-09-26 02:47
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models, transaction
|
||||
from acls.models import LoginACL
|
||||
|
||||
LOGIN_CONFIRM_ZH = '登录复核'
|
||||
LOGIN_CONFIRM_EN = 'Login confirm'
|
||||
|
||||
DEFAULT_TIME_PERIODS = [{'id': i, 'value': '00:00~00:00'} for i in range(7)]
|
||||
|
||||
|
||||
def has_zh(name: str) -> bool:
|
||||
for i in name:
|
||||
if u'\u4e00' <= i <= u'\u9fff':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def migrate_login_confirm(apps, schema_editor):
|
||||
login_acl_model = apps.get_model("acls", "LoginACL")
|
||||
login_confirm_model = apps.get_model("authentication", "LoginConfirmSetting")
|
||||
|
||||
with transaction.atomic():
|
||||
for instance in login_confirm_model.objects.filter(is_active=True):
|
||||
user = instance.user
|
||||
reviewers = instance.reviewers.all()
|
||||
login_confirm = LOGIN_CONFIRM_ZH if has_zh(user.name) else LOGIN_CONFIRM_EN
|
||||
date_created = instance.date_created.strftime('%Y-%m-%d %H:%M:%S')
|
||||
if reviewers.count() == 0:
|
||||
continue
|
||||
data = {
|
||||
'user': user,
|
||||
'name': f'{user.name}-{login_confirm} ({date_created})',
|
||||
'created_by': instance.created_by,
|
||||
'action': LoginACL.ActionChoices.confirm,
|
||||
'rules': {'ip_group': ['*'], 'time_period': DEFAULT_TIME_PERIODS}
|
||||
}
|
||||
instance = login_acl_model.objects.create(**data)
|
||||
instance.reviewers.set(reviewers)
|
||||
|
||||
|
||||
def migrate_ip_group(apps, schema_editor):
|
||||
login_acl_model = apps.get_model("acls", "LoginACL")
|
||||
updates = list()
|
||||
with transaction.atomic():
|
||||
for instance in login_acl_model.objects.exclude(action=LoginACL.ActionChoices.confirm):
|
||||
instance.rules = {'ip_group': instance.ip_group, 'time_period': DEFAULT_TIME_PERIODS}
|
||||
updates.append(instance)
|
||||
login_acl_model.objects.bulk_update(updates, ['rules', ])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('acls', '0001_initial'),
|
||||
('authentication', '0004_ssotoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='loginacl',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Login confirm')],
|
||||
default='reject', max_length=64, verbose_name='Action'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='loginacl',
|
||||
name='reviewers',
|
||||
field=models.ManyToManyField(blank=True, related_name='login_confirm_acls',
|
||||
to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='loginacl',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='loginacl',
|
||||
name='rules',
|
||||
field=models.JSONField(default=dict, verbose_name='Rule'),
|
||||
),
|
||||
migrations.RunPython(migrate_login_confirm),
|
||||
migrations.RunPython(migrate_ip_group),
|
||||
migrations.RemoveField(
|
||||
model_name='loginacl',
|
||||
name='ip_group',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='loginacl',
|
||||
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login acl'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='loginassetacl',
|
||||
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login asset acl'},
|
||||
),
|
||||
]
|
||||
0
apps/acls/migrations/__init__.py
Normal file
0
apps/acls/migrations/__init__.py
Normal file
2
apps/acls/models/__init__.py
Normal file
2
apps/acls/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
35
apps/acls/models/base.py
Normal file
35
apps/acls/models/base.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from common.mixins import CommonModelMixin
|
||||
|
||||
|
||||
__all__ = ['BaseACL', 'BaseACLQuerySet']
|
||||
|
||||
|
||||
class BaseACLQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def inactive(self):
|
||||
return self.filter(is_active=False)
|
||||
|
||||
def valid(self):
|
||||
return self.active()
|
||||
|
||||
def invalid(self):
|
||||
return self.inactive()
|
||||
|
||||
|
||||
class BaseACL(CommonModelMixin):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
priority = models.IntegerField(
|
||||
default=50, verbose_name=_("Priority"),
|
||||
help_text=_("1-100, the lower the value will be match first"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
128
apps/acls/models/login_acl.py
Normal file
128
apps/acls/models/login_acl.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from common.utils import get_request_ip, get_ip_city
|
||||
from common.utils.ip import contains_ip
|
||||
from common.utils.time_period import contains_time_period
|
||||
from common.utils.timezone import local_now_display
|
||||
|
||||
|
||||
class ACLManager(models.Manager):
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class LoginACL(BaseACL):
|
||||
class ActionChoices(models.TextChoices):
|
||||
reject = 'reject', _('Reject')
|
||||
allow = 'allow', _('Allow')
|
||||
confirm = 'confirm', _('Login confirm')
|
||||
|
||||
# 用户
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.CASCADE, verbose_name=_('User'),
|
||||
related_name='login_acls'
|
||||
)
|
||||
# 规则
|
||||
rules = models.JSONField(default=dict, verbose_name=_('Rule'))
|
||||
# 动作
|
||||
action = models.CharField(
|
||||
max_length=64, verbose_name=_('Action'),
|
||||
choices=ActionChoices.choices, default=ActionChoices.reject
|
||||
)
|
||||
reviewers = models.ManyToManyField(
|
||||
'users.User', verbose_name=_("Reviewers"),
|
||||
related_name="login_confirm_acls", blank=True
|
||||
)
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
|
||||
class Meta:
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
verbose_name = _('Login acl')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
||||
@property
|
||||
def action_allow(self):
|
||||
return self.action == self.ActionChoices.allow
|
||||
|
||||
@classmethod
|
||||
def filter_acl(cls, user):
|
||||
return user.login_acls.all().valid().distinct()
|
||||
|
||||
@staticmethod
|
||||
def allow_user_confirm_if_need(user, ip):
|
||||
acl = LoginACL.filter_acl(user).filter(
|
||||
action=LoginACL.ActionChoices.confirm
|
||||
).first()
|
||||
acl = acl if acl and acl.reviewers.exists() else None
|
||||
if not acl:
|
||||
return False, acl
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
return is_contain_ip and is_contain_time_period, acl
|
||||
|
||||
@staticmethod
|
||||
def allow_user_to_login(user, ip):
|
||||
acl = LoginACL.filter_acl(user).exclude(
|
||||
action=LoginACL.ActionChoices.confirm
|
||||
).first()
|
||||
if not acl:
|
||||
return True, ''
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
|
||||
reject_type = ''
|
||||
if is_contain_ip and is_contain_time_period:
|
||||
# 满足条件
|
||||
allow = acl.action_allow
|
||||
if not allow:
|
||||
reject_type = 'ip' if is_contain_ip else 'time'
|
||||
else:
|
||||
# 不满足条件
|
||||
# 如果acl本身允许,那就拒绝;如果本身拒绝,那就允许
|
||||
allow = not acl.action_allow
|
||||
if not allow:
|
||||
reject_type = 'ip' if not is_contain_ip else 'time'
|
||||
|
||||
return allow, reject_type
|
||||
|
||||
@staticmethod
|
||||
def construct_confirm_ticket_meta(request=None):
|
||||
login_ip = get_request_ip(request) if request else ''
|
||||
login_ip = login_ip or '0.0.0.0'
|
||||
login_city = get_ip_city(login_ip)
|
||||
login_datetime = local_now_display()
|
||||
ticket_meta = {
|
||||
'apply_login_ip': login_ip,
|
||||
'apply_login_city': login_city,
|
||||
'apply_login_datetime': login_datetime,
|
||||
}
|
||||
return ticket_meta
|
||||
|
||||
def create_confirm_ticket(self, request=None):
|
||||
from tickets import const
|
||||
from tickets.models import Ticket
|
||||
from orgs.models import Organization
|
||||
ticket_title = _('Login confirm') + ' {}'.format(self.user)
|
||||
ticket_meta = self.construct_confirm_ticket_meta(request)
|
||||
data = {
|
||||
'title': ticket_title,
|
||||
'type': const.TicketType.login_confirm.value,
|
||||
'meta': ticket_meta,
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(self.user)
|
||||
return ticket
|
||||
103
apps/acls/models/login_asset_acl.py
Normal file
103
apps/acls/models/login_asset_acl.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from common.utils.ip import contains_ip
|
||||
|
||||
|
||||
class ACLManager(OrgManager):
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
class ActionChoices(models.TextChoices):
|
||||
login_confirm = 'login_confirm', _('Login confirm')
|
||||
|
||||
# 条件
|
||||
users = models.JSONField(verbose_name=_('User'))
|
||||
system_users = models.JSONField(verbose_name=_('System User'))
|
||||
assets = models.JSONField(verbose_name=_('Asset'))
|
||||
# 动作
|
||||
action = models.CharField(
|
||||
max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm,
|
||||
verbose_name=_('Action')
|
||||
)
|
||||
# 动作: 附加字段
|
||||
# - login_confirm
|
||||
reviewers = models.ManyToManyField(
|
||||
'users.User', related_name='review_login_asset_acls', blank=True,
|
||||
verbose_name=_("Reviewers")
|
||||
)
|
||||
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('name', 'org_id')
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
verbose_name = _('Login asset acl')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def filter(cls, user, asset, system_user, action):
|
||||
queryset = cls.objects.filter(action=action)
|
||||
queryset = cls.filter_user(user, queryset)
|
||||
queryset = cls.filter_asset(asset, queryset)
|
||||
queryset = cls.filter_system_user(system_user, queryset)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_user(cls, user, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(users__username_group__contains=user.username) |
|
||||
Q(users__username_group__contains='*')
|
||||
)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_asset(cls, asset, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(assets__hostname_group__contains=asset.hostname) |
|
||||
Q(assets__hostname_group__contains='*')
|
||||
)
|
||||
ids = [q.id for q in queryset if contains_ip(asset.ip, q.assets.get('ip_group', []))]
|
||||
queryset = cls.objects.filter(id__in=ids)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_system_user(cls, system_user, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(system_users__name_group__contains=system_user.name) |
|
||||
Q(system_users__name_group__contains='*')
|
||||
).filter(
|
||||
Q(system_users__username_group__contains=system_user.username) |
|
||||
Q(system_users__username_group__contains='*')
|
||||
).filter(
|
||||
Q(system_users__protocol_group__contains=system_user.protocol) |
|
||||
Q(system_users__protocol_group__contains='*')
|
||||
)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
|
||||
from tickets.const import TicketType
|
||||
from tickets.models import Ticket
|
||||
data = {
|
||||
'title': _('Login asset confirm') + ' ({})'.format(user),
|
||||
'type': TicketType.login_asset_confirm,
|
||||
'meta': {
|
||||
'apply_login_user': str(user),
|
||||
'apply_login_asset': str(asset),
|
||||
'apply_login_system_user': str(system_user),
|
||||
},
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(assignees)
|
||||
ticket.open(applicant=user)
|
||||
return ticket
|
||||
|
||||
3
apps/acls/serializers/__init__.py
Normal file
3
apps/acls/serializers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .login_asset_check import *
|
||||
56
apps/acls/serializers/login_acl.py
Normal file
56
apps/acls/serializers/login_acl.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from ..models import LoginACL
|
||||
from .rules import RuleSerializer
|
||||
|
||||
__all__ = ['LoginACLSerializer', ]
|
||||
|
||||
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
|
||||
|
||||
|
||||
class LoginACLSerializer(BulkModelSerializer):
|
||||
user_display = serializers.ReadOnlyField(source='user.username', label=_('Username'))
|
||||
reviewers_display = serializers.SerializerMethodField(label=_('Reviewers'))
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count')
|
||||
rules = MethodSerializer()
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'priority', 'rules', 'action', 'action_display',
|
||||
'is_active', 'user', 'user_display',
|
||||
'date_created', 'date_updated', 'reviewers_amount',
|
||||
'comment', 'created_by'
|
||||
]
|
||||
fields_fk = ['user', 'user_display']
|
||||
fields_m2m = ['reviewers', 'reviewers_display']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
extra_kwargs = {
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_action_choices()
|
||||
|
||||
def set_action_choices(self):
|
||||
action = self.fields.get('action')
|
||||
if not action:
|
||||
return
|
||||
choices = action._choices
|
||||
if not has_valid_xpack_license():
|
||||
choices.pop(LoginACL.ActionChoices.confirm, None)
|
||||
action._choices = choices
|
||||
|
||||
def get_rules_serializer(self):
|
||||
return RuleSerializer()
|
||||
|
||||
def get_reviewers_display(self, obj):
|
||||
return ','.join([str(user) for user in obj.reviewers.all()])
|
||||
105
apps/acls/serializers/login_asset_acl.py
Normal file
105
apps/acls/serializers/login_asset_acl.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.models import SystemUser
|
||||
from acls import models
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
__all__ = ['LoginAssetACLSerializer']
|
||||
|
||||
|
||||
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
|
||||
|
||||
|
||||
class LoginAssetACLUsersSerializer(serializers.Serializer):
|
||||
username_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
|
||||
help_text=common_help_text
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetACLAssestsSerializer(serializers.Serializer):
|
||||
ip_group_help_text = _(
|
||||
'Format for comma-delimited string, with * indicating a match all. '
|
||||
'Such as: '
|
||||
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
|
||||
'(Domain name support)'
|
||||
)
|
||||
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'),
|
||||
help_text=ip_group_help_text
|
||||
)
|
||||
hostname_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'),
|
||||
help_text=common_help_text
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
||||
protocol_group_help_text = _(
|
||||
'Format for comma-delimited string, with * indicating a match all. '
|
||||
'Protocol options: {}'
|
||||
)
|
||||
|
||||
name_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'),
|
||||
help_text=common_help_text
|
||||
)
|
||||
username_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
|
||||
help_text=common_help_text
|
||||
)
|
||||
protocol_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||
help_text=protocol_group_help_text.format(
|
||||
', '.join([SystemUser.Protocol.ssh, SystemUser.Protocol.telnet])
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_protocol_group(protocol_group):
|
||||
unsupported_protocols = set(protocol_group) - set(SystemUser.ASSET_CATEGORY_PROTOCOLS + ['*'])
|
||||
if unsupported_protocols:
|
||||
error = _('Unsupported protocols: {}').format(unsupported_protocols)
|
||||
raise serializers.ValidationError(error)
|
||||
return protocol_group
|
||||
|
||||
|
||||
class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
|
||||
users = LoginAssetACLUsersSerializer()
|
||||
assets = LoginAssetACLAssestsSerializer()
|
||||
system_users = LoginAssetACLSystemUsersSerializer()
|
||||
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
|
||||
class Meta:
|
||||
model = models.LoginAssetACL
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'users', 'system_users', 'assets',
|
||||
'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id'
|
||||
]
|
||||
fields_m2m = ['reviewers', 'reviewers_amount']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
}
|
||||
|
||||
def validate_reviewers(self, reviewers):
|
||||
org_id = self.fields['org_id'].default()
|
||||
org = Organization.get_instance(org_id)
|
||||
if not org:
|
||||
error = _('The organization `{}` does not exist'.format(org_id))
|
||||
raise serializers.ValidationError(error)
|
||||
users = org.get_members()
|
||||
valid_reviewers = list(set(reviewers) & set(users))
|
||||
if not valid_reviewers:
|
||||
error = _('None of the reviewers belong to Organization `{}`'.format(org.name))
|
||||
raise serializers.ValidationError(error)
|
||||
return valid_reviewers
|
||||
71
apps/acls/serializers/login_asset_check.py
Normal file
71
apps/acls/serializers/login_asset_check.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.utils import get_object_or_none, lazyproperty
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser
|
||||
|
||||
|
||||
__all__ = ['LoginAssetCheckSerializer']
|
||||
|
||||
|
||||
class LoginAssetCheckSerializer(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
asset_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
system_user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
system_user_username = serializers.CharField(max_length=128, default='')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
self.asset = None
|
||||
self._system_user = None
|
||||
self._system_user_username = None
|
||||
|
||||
def validate_user_id(self, user_id):
|
||||
self.user = self.validate_object_exist(User, user_id)
|
||||
return user_id
|
||||
|
||||
def validate_asset_id(self, asset_id):
|
||||
self.asset = self.validate_object_exist(Asset, asset_id)
|
||||
return asset_id
|
||||
|
||||
def validate_system_user_id(self, system_user_id):
|
||||
self._system_user = self.validate_object_exist(SystemUser, system_user_id)
|
||||
return system_user_id
|
||||
|
||||
def validate_system_user_username(self, system_user_username):
|
||||
system_user_id = self.initial_data.get('system_user_id')
|
||||
system_user = self.validate_object_exist(SystemUser, system_user_id)
|
||||
if self._system_user.login_mode == SystemUser.LOGIN_MANUAL \
|
||||
and not system_user.username \
|
||||
and not system_user.username_same_with_user \
|
||||
and not system_user_username:
|
||||
error = 'Missing parameter: system_user_username'
|
||||
raise serializers.ValidationError(error)
|
||||
self._system_user_username = system_user_username
|
||||
return system_user_username
|
||||
|
||||
@staticmethod
|
||||
def validate_object_exist(model, field_id):
|
||||
with tmp_to_root_org():
|
||||
obj = get_object_or_none(model, pk=field_id)
|
||||
if not obj:
|
||||
error = '{} Model object does not exist'.format(model.__name__)
|
||||
raise serializers.ValidationError(error)
|
||||
return obj
|
||||
|
||||
@lazyproperty
|
||||
def system_user(self):
|
||||
if self._system_user.username_same_with_user:
|
||||
username = self.user.username
|
||||
elif self._system_user.login_mode == SystemUser.LOGIN_MANUAL:
|
||||
username = self._system_user_username
|
||||
else:
|
||||
username = self._system_user.username
|
||||
self._system_user.username = username
|
||||
return self._system_user
|
||||
|
||||
@lazyproperty
|
||||
def org(self):
|
||||
return self.asset.org
|
||||
1
apps/acls/serializers/rules/__init__.py
Normal file
1
apps/acls/serializers/rules/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .rules import *
|
||||
35
apps/acls/serializers/rules/rules.py
Normal file
35
apps/acls/serializers/rules/rules.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text']
|
||||
|
||||
|
||||
def ip_group_child_validator(ip_group_child):
|
||||
is_valid = ip_group_child == '*' \
|
||||
or is_ip_address(ip_group_child) \
|
||||
or is_ip_network(ip_group_child) \
|
||||
or is_ip_segment(ip_group_child)
|
||||
if not is_valid:
|
||||
error = _('IP address invalid: `{}`').format(ip_group_child)
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
|
||||
ip_group_help_text = _(
|
||||
'Format for comma-delimited string, with * indicating a match all. '
|
||||
'Such as: '
|
||||
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
|
||||
)
|
||||
|
||||
|
||||
class RuleSerializer(serializers.Serializer):
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], label=_('IP'), help_text=ip_group_help_text,
|
||||
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]))
|
||||
time_period = serializers.ListField(default=[], label=_('Time Period'))
|
||||
3
apps/acls/tests.py
Normal file
3
apps/acls/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
1
apps/acls/urls/__init__.py
Normal file
1
apps/acls/urls/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .api_urls import *
|
||||
18
apps/acls/urls/api_urls.py
Normal file
18
apps/acls/urls/api_urls.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.urls import path
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
from .. import api
|
||||
|
||||
|
||||
app_name = 'acls'
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'login-acls', api.LoginACLViewSet, 'login-acl')
|
||||
router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl')
|
||||
|
||||
urlpatterns = [
|
||||
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
|
||||
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
0
apps/acls/utils.py
Normal file
0
apps/acls/utils.py
Normal file
@@ -1,3 +1,4 @@
|
||||
from .application import *
|
||||
from .account import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
58
apps/applications/api/account.py
Normal file
58
apps/applications/api/account.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django_filters import rest_framework as filters
|
||||
from django.db.models import F, Q
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from ..models import Account
|
||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
username = filters.CharFilter(method='do_nothing')
|
||||
type = filters.CharFilter(field_name='type', lookup_expr='exact')
|
||||
category = filters.CharFilter(field_name='category', lookup_expr='exact')
|
||||
app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact')
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ['app', 'systemuser']
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
qs = super().qs
|
||||
qs = self.filter_username(qs)
|
||||
return qs
|
||||
|
||||
def filter_username(self, qs):
|
||||
username = self.get_query_param('username')
|
||||
if not username:
|
||||
return qs
|
||||
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
||||
return qs
|
||||
|
||||
|
||||
class ApplicationAccountViewSet(JMSBulkModelViewSet):
|
||||
model = Account
|
||||
search_fields = ['username', 'app_display']
|
||||
filterset_class = AccountFilterSet
|
||||
filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
|
||||
serializer_class = serializers.AppAccountSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Account.objects.all() \
|
||||
.annotate(type=F('app__type')) \
|
||||
.annotate(app_display=F('app__name')) \
|
||||
.annotate(systemuser_display=F('systemuser__name')) \
|
||||
.annotate(category=F('app__category'))
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
http_method_names = ['get', 'options']
|
||||
38
apps/applications/api/application.py
Normal file
38
apps/applications/api/application.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import serializers
|
||||
from ..models import Application
|
||||
|
||||
__all__ = ['ApplicationViewSet']
|
||||
|
||||
|
||||
class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Application
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'category': ['exact'],
|
||||
'type': ['exact', 'in'],
|
||||
}
|
||||
search_fields = ('name', 'type', 'category')
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': serializers.AppSerializer,
|
||||
'get_tree': TreeNodeSerializer,
|
||||
'suggestion': serializers.MiniAppSerializer
|
||||
}
|
||||
|
||||
@action(methods=['GET'], detail=False, url_path='tree')
|
||||
def get_tree(self, request, *args, **kwargs):
|
||||
show_count = request.query_params.get('show_count', '1') == '1'
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
|
||||
serializer = self.get_serializer(tree_nodes, many=True)
|
||||
return Response(serializer.data)
|
||||
@@ -1,20 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'DatabaseAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.DatabaseApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DatabaseAppSerializer
|
||||
@@ -1,20 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'K8sAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSerializer
|
||||
55
apps/applications/api/mixin.py
Normal file
55
apps/applications/api/mixin.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.tree import TreeNode
|
||||
from orgs.models import Organization
|
||||
from ..models import Application
|
||||
|
||||
__all__ = ['SerializeApplicationToTreeNodeMixin']
|
||||
|
||||
|
||||
class SerializeApplicationToTreeNodeMixin:
|
||||
@staticmethod
|
||||
def filter_organizations(applications):
|
||||
organization_ids = set(applications.values_list('org_id', flat=True))
|
||||
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
|
||||
organizations.sort(key=lambda x: x.name)
|
||||
return organizations
|
||||
|
||||
@staticmethod
|
||||
def create_root_node():
|
||||
name = _('My applications')
|
||||
node = TreeNode(**{
|
||||
'id': 'applications',
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': '',
|
||||
'open': True,
|
||||
'isParent': True,
|
||||
'meta': {
|
||||
'type': 'root'
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
def serialize_applications_with_org(self, applications):
|
||||
if not applications:
|
||||
return []
|
||||
root_node = self.create_root_node()
|
||||
tree_nodes = [root_node]
|
||||
organizations = self.filter_organizations(applications)
|
||||
|
||||
for i, org in enumerate(organizations):
|
||||
# 组织节点
|
||||
org_node = org.as_tree_node(pid=root_node.id)
|
||||
tree_nodes.append(org_node)
|
||||
org_applications = applications.filter(org_id=org.id)
|
||||
count = org_applications.count()
|
||||
org_node.name += '({})'.format(count)
|
||||
|
||||
# 各应用节点
|
||||
apps_nodes = Application.create_tree_nodes(
|
||||
queryset=org_applications, root_node=org_node,
|
||||
show_empty=False
|
||||
)
|
||||
tree_nodes += apps_nodes
|
||||
return tree_nodes
|
||||
@@ -1,27 +1,19 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import RemoteApp
|
||||
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
|
||||
from ..hands import IsAppUser
|
||||
from .. import models
|
||||
from ..serializers import RemoteAppConnectionInfoSerializer
|
||||
from ..permissions import IsRemoteApp
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteAppViewSet', 'RemoteAppConnectionInfoApi',
|
||||
'RemoteAppConnectionInfoApi',
|
||||
]
|
||||
|
||||
|
||||
class RemoteAppViewSet(OrgBulkModelViewSet):
|
||||
model = RemoteApp
|
||||
filter_fields = ('name', 'type', 'comment')
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = RemoteAppSerializer
|
||||
|
||||
|
||||
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
|
||||
model = RemoteApp
|
||||
permission_classes = (IsAppUser, )
|
||||
model = models.Application
|
||||
permission_classes = (IsAppUser, IsRemoteApp)
|
||||
serializer_class = RemoteAppConnectionInfoSerializer
|
||||
|
||||
@@ -1,64 +1,67 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
# RemoteApp
|
||||
class AppCategory(TextChoices):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
|
||||
REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor'
|
||||
|
||||
REMOTE_APP_TYPE_CHROME = 'chrome'
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench'
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client'
|
||||
REMOTE_APP_TYPE_CUSTOM = 'custom'
|
||||
|
||||
# Fields attribute write_only default => False
|
||||
|
||||
REMOTE_APP_TYPE_CHROME_FIELDS = [
|
||||
{'name': 'chrome_target'},
|
||||
{'name': 'chrome_username'},
|
||||
{'name': 'chrome_password', 'write_only': True}
|
||||
]
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
|
||||
{'name': 'mysql_workbench_ip'},
|
||||
{'name': 'mysql_workbench_name'},
|
||||
{'name': 'mysql_workbench_port'},
|
||||
{'name': 'mysql_workbench_username'},
|
||||
{'name': 'mysql_workbench_password', 'write_only': True}
|
||||
]
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS = [
|
||||
{'name': 'vmware_target'},
|
||||
{'name': 'vmware_username'},
|
||||
{'name': 'vmware_password', 'write_only': True}
|
||||
]
|
||||
REMOTE_APP_TYPE_CUSTOM_FIELDS = [
|
||||
{'name': 'custom_cmdline'},
|
||||
{'name': 'custom_target'},
|
||||
{'name': 'custom_username'},
|
||||
{'name': 'custom_password', 'write_only': True}
|
||||
]
|
||||
|
||||
REMOTE_APP_TYPE_FIELDS_MAP = {
|
||||
REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS,
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS,
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS,
|
||||
REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS
|
||||
}
|
||||
|
||||
REMOTE_APP_TYPE_CHOICES = (
|
||||
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
|
||||
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
|
||||
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
|
||||
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
|
||||
)
|
||||
@classmethod
|
||||
def get_label(cls, category):
|
||||
return dict(cls.choices).get(category, '')
|
||||
|
||||
|
||||
# DatabaseApp
|
||||
class AppType(TextChoices):
|
||||
# db category
|
||||
mysql = 'mysql', 'MySQL'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
pgsql = 'postgresql', 'PostgreSQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
|
||||
# remote-app category
|
||||
chrome = 'chrome', 'Chrome'
|
||||
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
|
||||
vmware_client = 'vmware_client', 'vSphere Client'
|
||||
custom = 'custom', _('Custom')
|
||||
|
||||
# cloud category
|
||||
k8s = 'k8s', 'Kubernetes'
|
||||
|
||||
@classmethod
|
||||
def category_types_mapper(cls):
|
||||
return {
|
||||
AppCategory.db: [cls.mysql, cls.oracle, cls.pgsql, cls.mariadb],
|
||||
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
|
||||
AppCategory.cloud: [cls.k8s]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_category_mapper(cls):
|
||||
mapper = {}
|
||||
for category, tps in cls.category_types_mapper().items():
|
||||
for tp in tps:
|
||||
mapper[tp] = category
|
||||
return mapper
|
||||
|
||||
@classmethod
|
||||
def get_label(cls, tp):
|
||||
return dict(cls.choices).get(tp, '')
|
||||
|
||||
@classmethod
|
||||
def db_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]]
|
||||
|
||||
@classmethod
|
||||
def remote_app_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]]
|
||||
|
||||
@classmethod
|
||||
def cloud_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
|
||||
|
||||
|
||||
|
||||
DATABASE_APP_TYPE_MYSQL = 'mysql'
|
||||
|
||||
DATABASE_APP_TYPE_CHOICES = (
|
||||
(DATABASE_APP_TYPE_MYSQL, 'MySQL'),
|
||||
)
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
||||
140
apps/applications/migrations/0006_application.py
Normal file
140
apps/applications/migrations/0006_application.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# Generated by Django 2.2.13 on 2020-10-19 12:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_mysql.models
|
||||
import uuid
|
||||
|
||||
|
||||
CATEGORY_DB_LIST = ['mysql', 'oracle', 'postgresql', 'mariadb']
|
||||
CATEGORY_REMOTE_LIST = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']
|
||||
CATEGORY_CLOUD_LIST = ['k8s']
|
||||
|
||||
CATEGORY_DB = 'db'
|
||||
CATEGORY_REMOTE = 'remote_app'
|
||||
CATEGORY_CLOUD = 'cloud'
|
||||
CATEGORY_LIST = [CATEGORY_DB, CATEGORY_REMOTE, CATEGORY_CLOUD]
|
||||
|
||||
|
||||
def get_application_category(old_app):
|
||||
_type = old_app.type
|
||||
if _type in CATEGORY_DB_LIST:
|
||||
category = CATEGORY_DB
|
||||
elif _type in CATEGORY_REMOTE_LIST:
|
||||
category = CATEGORY_REMOTE
|
||||
elif _type in CATEGORY_CLOUD_LIST:
|
||||
category = CATEGORY_CLOUD
|
||||
else:
|
||||
category = None
|
||||
return category
|
||||
|
||||
|
||||
def common_to_application_json(old_app):
|
||||
category = get_application_category(old_app)
|
||||
date_updated = old_app.date_updated if hasattr(old_app, 'date_updated') else old_app.date_created
|
||||
return {
|
||||
'id': old_app.id,
|
||||
'name': old_app.name,
|
||||
'type': old_app.type,
|
||||
'category': category,
|
||||
'comment': old_app.comment,
|
||||
'created_by': old_app.created_by,
|
||||
'date_created': old_app.date_created,
|
||||
'date_updated': date_updated,
|
||||
'org_id': old_app.org_id
|
||||
}
|
||||
|
||||
|
||||
def db_to_application_json(database):
|
||||
app_json = common_to_application_json(database)
|
||||
app_json.update({
|
||||
'attrs': {
|
||||
'host': database.host,
|
||||
'port': database.port,
|
||||
'database': database.database
|
||||
}
|
||||
})
|
||||
return app_json
|
||||
|
||||
|
||||
def remote_to_application_json(remote):
|
||||
app_json = common_to_application_json(remote)
|
||||
attrs = {
|
||||
'asset': str(remote.asset.id),
|
||||
'path': remote.path,
|
||||
}
|
||||
attrs.update(remote.params)
|
||||
app_json.update({
|
||||
'attrs': attrs
|
||||
})
|
||||
return app_json
|
||||
|
||||
|
||||
def k8s_to_application_json(k8s):
|
||||
app_json = common_to_application_json(k8s)
|
||||
app_json.update({
|
||||
'attrs': {
|
||||
'cluster': k8s.cluster
|
||||
}
|
||||
})
|
||||
return app_json
|
||||
|
||||
|
||||
def migrate_and_integrate_applications(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
database_app_model = apps.get_model("applications", "DatabaseApp")
|
||||
remote_app_model = apps.get_model("applications", "RemoteApp")
|
||||
k8s_app_model = apps.get_model("applications", "K8sApp")
|
||||
|
||||
database_apps = database_app_model.objects.using(db_alias).all()
|
||||
remote_apps = remote_app_model.objects.using(db_alias).all()
|
||||
k8s_apps = k8s_app_model.objects.using(db_alias).all()
|
||||
|
||||
database_applications = [db_to_application_json(db_app) for db_app in database_apps]
|
||||
remote_applications = [remote_to_application_json(remote_app) for remote_app in remote_apps]
|
||||
k8s_applications = [k8s_to_application_json(k8s_app) for k8s_app in k8s_apps]
|
||||
|
||||
applications_json = database_applications + remote_applications + k8s_applications
|
||||
application_model = apps.get_model("applications", "Application")
|
||||
applications = [
|
||||
application_model(**application_json)
|
||||
for application_json in applications_json
|
||||
if application_json['category'] in CATEGORY_LIST
|
||||
]
|
||||
for application in applications:
|
||||
if application_model.objects.using(db_alias).filter(name=application.name).exists():
|
||||
application.name = '{}-{}'.format(application.name, application.type)
|
||||
application.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0057_fill_node_value_assets_amount_and_parent_key'),
|
||||
('applications', '0005_k8sapp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
|
||||
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
|
||||
('attrs', django_mysql.models.JSONField(default=dict)),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
'unique_together': {('org_id', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.RunPython(migrate_and_integrate_applications),
|
||||
]
|
||||
18
apps/applications/migrations/0007_auto_20201119_1110.py
Normal file
18
apps/applications/migrations/0007_auto_20201119_1110.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-11-19 03:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0006_application'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='attrs',
|
||||
field=models.JSONField(),
|
||||
),
|
||||
]
|
||||
28
apps/applications/migrations/0008_auto_20210104_0435.py
Normal file
28
apps/applications/migrations/0008_auto_20210104_0435.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.1 on 2021-01-03 20:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('perms', '0017_auto_20210104_0435'),
|
||||
('applications', '0007_auto_20201119_1110'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='DatabaseApp',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='K8sApp',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='attrs',
|
||||
field=models.JSONField(default=dict, verbose_name='Attrs'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='RemoteApp',
|
||||
),
|
||||
]
|
||||
25
apps/applications/migrations/0009_applicationuser.py
Normal file
25
apps/applications/migrations/0009_applicationuser.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-23 09:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0070_auto_20210426_1515'),
|
||||
('applications', '0008_auto_20210104_0435'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApplicationUser',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('assets.systemuser',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Generated by Django 3.1.12 on 2021-08-26 09:07
|
||||
|
||||
import assets.models.base
|
||||
import common.fields.model
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0076_delete_assetuser'),
|
||||
('applications', '0009_applicationuser'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HistoricalAccount',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('version', models.IntegerField(default=1, verbose_name='Version')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField()),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('app', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Database')),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Account',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': 'history_date',
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('version', models.IntegerField(default=1, verbose_name='Version')),
|
||||
('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Database')),
|
||||
('systemuser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account',
|
||||
'unique_together': {('username', 'app', 'systemuser')},
|
||||
},
|
||||
bases=(models.Model, assets.models.base.AuthMixin),
|
||||
),
|
||||
]
|
||||
40
apps/applications/migrations/0011_auto_20210826_1759.py
Normal file
40
apps/applications/migrations/0011_auto_20210826_1759.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 3.1.12 on 2021-08-26 09:59
|
||||
|
||||
from django.db import migrations, transaction
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def migrate_app_account(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
app_perm_model = apps.get_model("perms", "ApplicationPermission")
|
||||
app_account_model = apps.get_model("applications", 'Account')
|
||||
|
||||
queryset = app_perm_model.objects \
|
||||
.exclude(system_users__isnull=True) \
|
||||
.exclude(applications__isnull=True) \
|
||||
.annotate(systemuser=F('system_users')) \
|
||||
.annotate(app=F('applications')) \
|
||||
.values('app', 'systemuser', 'org_id')
|
||||
|
||||
accounts = []
|
||||
for p in queryset:
|
||||
if not p['app']:
|
||||
continue
|
||||
account = app_account_model(
|
||||
app_id=p['app'], systemuser_id=p['systemuser'],
|
||||
version=1, org_id=p['org_id']
|
||||
)
|
||||
accounts.append(account)
|
||||
|
||||
app_account_model.objects.using(db_alias).bulk_create(accounts, ignore_conflicts=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0010_appaccount_historicalappaccount'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_app_account)
|
||||
]
|
||||
23
apps/applications/migrations/0012_auto_20211014_2209.py
Normal file
23
apps/applications/migrations/0012_auto_20211014_2209.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-14 14:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0011_auto_20210826_1759'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalaccount',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
17
apps/applications/migrations/0013_auto_20211026_1711.py
Normal file
17
apps/applications/migrations/0013_auto_20211026_1711.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-26 09:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0012_auto_20211014_2209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='application',
|
||||
options={'ordering': ('name',), 'verbose_name': 'Application'},
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,2 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
from .application import *
|
||||
from .account import *
|
||||
|
||||
88
apps/applications/models/account.py
Normal file
88
apps/applications/models/account.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.db import models
|
||||
from simple_history.models import HistoricalRecords
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from assets.models.base import BaseUser
|
||||
|
||||
|
||||
class Account(BaseUser):
|
||||
app = models.ForeignKey('applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Database'))
|
||||
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
|
||||
version = models.IntegerField(default=1, verbose_name=_('Version'))
|
||||
history = HistoricalRecords()
|
||||
|
||||
auth_attrs = ['username', 'password', 'private_key', 'public_key']
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account')
|
||||
unique_together = [('username', 'app', 'systemuser')]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.auth_snapshot = {}
|
||||
|
||||
def get_or_systemuser_attr(self, attr):
|
||||
val = getattr(self, attr, None)
|
||||
if val:
|
||||
return val
|
||||
if self.systemuser:
|
||||
return getattr(self.systemuser, attr, '')
|
||||
return ''
|
||||
|
||||
def load_auth(self):
|
||||
for attr in self.auth_attrs:
|
||||
value = self.get_or_systemuser_attr(attr)
|
||||
self.auth_snapshot[attr] = [getattr(self, attr), value]
|
||||
setattr(self, attr, value)
|
||||
|
||||
def unload_auth(self):
|
||||
if not self.systemuser:
|
||||
return
|
||||
|
||||
for attr, values in self.auth_snapshot.items():
|
||||
origin_value, loaded_value = values
|
||||
current_value = getattr(self, attr, '')
|
||||
if current_value == loaded_value:
|
||||
setattr(self, attr, origin_value)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.unload_auth()
|
||||
instance = super().save(*args, **kwargs)
|
||||
self.load_auth()
|
||||
return instance
|
||||
|
||||
@lazyproperty
|
||||
def category(self):
|
||||
return self.app.category
|
||||
|
||||
@lazyproperty
|
||||
def type(self):
|
||||
return self.app.type
|
||||
|
||||
@lazyproperty
|
||||
def app_display(self):
|
||||
return self.systemuser.name
|
||||
|
||||
@property
|
||||
def username_display(self):
|
||||
return self.get_or_systemuser_attr('username') or ''
|
||||
|
||||
@lazyproperty
|
||||
def systemuser_display(self):
|
||||
if not self.systemuser:
|
||||
return ''
|
||||
return str(self.systemuser)
|
||||
|
||||
@property
|
||||
def smart_name(self):
|
||||
username = self.username_display
|
||||
|
||||
if self.app:
|
||||
app = str(self.app)
|
||||
else:
|
||||
app = '*'
|
||||
return '{}@{}'.format(username, app)
|
||||
|
||||
def __str__(self):
|
||||
return self.smart_name
|
||||
231
apps/applications/models/application.py
Normal file
231
apps/applications/models/application.py
Normal file
@@ -0,0 +1,231 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.tree import TreeNode
|
||||
from assets.models import Asset, SystemUser
|
||||
from .. import const
|
||||
|
||||
|
||||
class ApplicationTreeNodeMixin:
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
category: str
|
||||
|
||||
@classmethod
|
||||
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
|
||||
show_empty=True, show_count=True):
|
||||
count = counts.get(c.value, 0)
|
||||
if count == 0 and not show_empty:
|
||||
return None
|
||||
label = c.label
|
||||
if count is not None and show_count:
|
||||
label = '{} ({})'.format(label, count)
|
||||
data = {
|
||||
'id': id_,
|
||||
'name': label,
|
||||
'title': label,
|
||||
'pId': pid,
|
||||
'isParent': bool(count),
|
||||
'open': opened,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': tp,
|
||||
'data': {
|
||||
'name': c.name,
|
||||
'value': c.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return TreeNode(**data)
|
||||
|
||||
@classmethod
|
||||
def create_root_tree_node(cls, queryset, show_count=True):
|
||||
count = queryset.count() if show_count else None
|
||||
root_id = 'applications'
|
||||
root_name = _('Applications')
|
||||
if count is not None and show_count:
|
||||
root_name = '{} ({})'.format(root_name, count)
|
||||
node = TreeNode(**{
|
||||
'id': root_id,
|
||||
'name': root_name,
|
||||
'title': root_name,
|
||||
'pId': '',
|
||||
'isParent': True,
|
||||
'open': True,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': 'applications_root',
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
categories = const.AppType.category_types_mapper().keys()
|
||||
for category in categories:
|
||||
i = root_node.id + '_' + category.value
|
||||
node = cls.create_choice_node(
|
||||
category, i, pid=root_node.id, tp='category',
|
||||
counts=counts, opened=False, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
type_category_mapper = const.AppType.type_category_mapper()
|
||||
for tp in const.AppType.type_category_mapper().keys():
|
||||
category = type_category_mapper.get(tp)
|
||||
pid = root_node.id + '_' + category.value
|
||||
i = root_node.id + '_' + tp.value
|
||||
node = cls.create_choice_node(
|
||||
tp, i, pid, tp='type', counts=counts, opened=False,
|
||||
show_empty=show_empty, show_count=show_count
|
||||
)
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def get_tree_node_counts(queryset):
|
||||
counts = defaultdict(int)
|
||||
values = queryset.values_list('type', 'category')
|
||||
for i in values:
|
||||
tp = i[0]
|
||||
category = i[1]
|
||||
counts[tp] += 1
|
||||
counts[category] += 1
|
||||
return counts
|
||||
|
||||
@classmethod
|
||||
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
|
||||
counts = cls.get_tree_node_counts(queryset)
|
||||
tree_nodes = []
|
||||
|
||||
# 根节点有可能是组织名称
|
||||
if root_node is None:
|
||||
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
|
||||
tree_nodes.append(root_node)
|
||||
|
||||
# 类别的节点
|
||||
tree_nodes += cls.create_category_tree_nodes(
|
||||
root_node, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
# 类型的节点
|
||||
tree_nodes += cls.create_types_tree_nodes(
|
||||
root_node, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
# 应用的节点
|
||||
for app in queryset:
|
||||
pid = root_node.id + '_' + app.type
|
||||
tree_nodes.append(app.as_tree_node(pid))
|
||||
return tree_nodes
|
||||
|
||||
def as_tree_node(self, pid):
|
||||
icon_skin_category_mapper = {
|
||||
'remote_app': 'chrome',
|
||||
'db': 'database',
|
||||
'cloud': 'cloud'
|
||||
}
|
||||
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
|
||||
node = TreeNode(**{
|
||||
'id': str(self.id),
|
||||
'name': self.name,
|
||||
'title': self.name,
|
||||
'pId': pid,
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': icon_skin,
|
||||
'meta': {
|
||||
'type': 'application',
|
||||
'data': {
|
||||
'category': self.category,
|
||||
'type': self.type,
|
||||
}
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
|
||||
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
category = models.CharField(
|
||||
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=16, choices=const.AppType.choices, verbose_name=_('Type')
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
'assets.Domain', null=True, blank=True, related_name='applications',
|
||||
on_delete=models.SET_NULL, verbose_name=_("Domain"),
|
||||
)
|
||||
attrs = models.JSONField(default=dict, verbose_name=_('Attrs'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Application')
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
category_display = self.get_category_display()
|
||||
type_display = self.get_type_display()
|
||||
return f'{self.name}({type_display})[{category_display}]'
|
||||
|
||||
@property
|
||||
def category_remote_app(self):
|
||||
return self.category == const.AppCategory.remote_app.value
|
||||
|
||||
def get_rdp_remote_app_setting(self):
|
||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||
if not self.category_remote_app:
|
||||
raise ValueError(f"Not a remote app application: {self.name}")
|
||||
serializer_class = get_serializer_class_by_application_type(self.type)
|
||||
fields = serializer_class().get_fields()
|
||||
|
||||
parameters = [self.type]
|
||||
for field_name in list(fields.keys()):
|
||||
if field_name in ['asset']:
|
||||
continue
|
||||
value = self.attrs.get(field_name)
|
||||
if not value:
|
||||
continue
|
||||
if field_name == 'path':
|
||||
value = '\"%s\"' % value
|
||||
parameters.append(str(value))
|
||||
|
||||
parameters = ' '.join(parameters)
|
||||
return {
|
||||
'program': '||jmservisor',
|
||||
'working_directory': '',
|
||||
'parameters': parameters
|
||||
}
|
||||
|
||||
def get_remote_app_asset(self):
|
||||
asset_id = self.attrs.get('asset')
|
||||
if not asset_id:
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
return asset
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
class Meta:
|
||||
proxy = True
|
||||
@@ -1,42 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['DatabaseApp']
|
||||
|
||||
|
||||
class DatabaseApp(CommonModelMixin, OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=const.DATABASE_APP_TYPE_MYSQL,
|
||||
choices=const.DATABASE_APP_TYPE_CHOICES,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
host = models.CharField(
|
||||
max_length=128, verbose_name=_('Host'), db_index=True
|
||||
)
|
||||
port = models.IntegerField(default=3306, verbose_name=_('Port'))
|
||||
database = models.CharField(
|
||||
max_length=128, blank=True, null=True, verbose_name=_('Database'),
|
||||
db_index=True
|
||||
)
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _("DatabaseApp")
|
||||
ordering = ('name', )
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db import models
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
class K8sApp(OrgModelMixin, models.JMSModel):
|
||||
class TYPE(models.ChoiceSet):
|
||||
K8S = 'k8s', _('Kubernetes')
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=TYPE.K8S, choices=TYPE.choices,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
cluster = models.CharField(max_length=1024, verbose_name=_('Cluster'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _('KubernetesApp')
|
||||
ordering = ('name', )
|
||||
@@ -1,78 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.fields.model import EncryptJsonDictTextField
|
||||
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteApp',
|
||||
]
|
||||
|
||||
|
||||
class RemoteApp(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
asset = models.ForeignKey(
|
||||
'assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')
|
||||
)
|
||||
type = models.CharField(
|
||||
default=const.REMOTE_APP_TYPE_CHROME,
|
||||
choices=const.REMOTE_APP_TYPE_CHOICES,
|
||||
max_length=128, verbose_name=_('App type')
|
||||
)
|
||||
path = models.CharField(
|
||||
max_length=128, blank=False, null=False,
|
||||
verbose_name=_('App path')
|
||||
)
|
||||
params = EncryptJsonDictTextField(
|
||||
max_length=4096, default={}, blank=True, null=True,
|
||||
verbose_name=_('Parameters')
|
||||
)
|
||||
created_by = models.CharField(
|
||||
max_length=32, null=True, blank=True, verbose_name=_('Created by')
|
||||
)
|
||||
date_created = models.DateTimeField(
|
||||
auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')
|
||||
)
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("RemoteApp")
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name', )
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
"""
|
||||
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
|
||||
"""
|
||||
_parameters = list()
|
||||
_parameters.append(self.type)
|
||||
path = '\"%s\"' % self.path
|
||||
_parameters.append(path)
|
||||
for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]:
|
||||
value = self.params.get(field['name'])
|
||||
if value is None:
|
||||
continue
|
||||
_parameters.append(value)
|
||||
_parameters = ' '.join(_parameters)
|
||||
return _parameters
|
||||
|
||||
@property
|
||||
def asset_info(self):
|
||||
return {
|
||||
'id': self.asset.id,
|
||||
'hostname': self.asset.hostname
|
||||
}
|
||||
9
apps/applications/permissions.py
Normal file
9
apps/applications/permissions.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
__all__ = ['IsRemoteApp']
|
||||
|
||||
|
||||
class IsRemoteApp(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return obj.category_remote_app
|
||||
@@ -1,3 +1,2 @@
|
||||
from .application import *
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
140
apps/applications/serializers/application.py
Normal file
140
apps/applications/serializers/application.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.serializers.base import AuthSerializerMixin
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from .attrs import (
|
||||
category_serializer_classes_mapping,
|
||||
type_serializer_classes_mapping
|
||||
)
|
||||
from .. import models
|
||||
from .. import const
|
||||
|
||||
__all__ = [
|
||||
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
|
||||
'AppAccountSerializer', 'AppAccountSecretSerializer'
|
||||
]
|
||||
|
||||
|
||||
class AppSerializerMixin(serializers.Serializer):
|
||||
attrs = MethodSerializer()
|
||||
|
||||
def get_attrs_serializer(self):
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
if isinstance(self.instance, models.Application):
|
||||
_type = self.instance.type
|
||||
_category = self.instance.category
|
||||
else:
|
||||
_type = self.context['request'].query_params.get('type')
|
||||
_category = self.context['request'].query_params.get('category')
|
||||
|
||||
if _type:
|
||||
serializer_class = type_serializer_classes_mapping.get(_type)
|
||||
elif _category:
|
||||
serializer_class = category_serializer_classes_mapping.get(_category)
|
||||
else:
|
||||
serializer_class = default_serializer
|
||||
|
||||
if not serializer_class:
|
||||
serializer_class = default_serializer
|
||||
|
||||
if isinstance(serializer_class, type):
|
||||
serializer = serializer_class()
|
||||
else:
|
||||
serializer = serializer_class
|
||||
return serializer
|
||||
|
||||
def create(self, validated_data):
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
|
||||
class Meta:
|
||||
model = models.Application
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'category', 'category_display', 'type', 'type_display',
|
||||
'attrs', 'date_created', 'date_updated', 'created_by', 'comment'
|
||||
]
|
||||
fields_fk = ['domain']
|
||||
fields = fields_small + fields_fk
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'date_updated', 'get_type_display',
|
||||
]
|
||||
|
||||
def validate_attrs(self, attrs):
|
||||
_attrs = self.instance.attrs if self.instance else {}
|
||||
_attrs.update(attrs)
|
||||
return _attrs
|
||||
|
||||
|
||||
class MiniAppSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Application
|
||||
fields = AppSerializer.Meta.fields_mini
|
||||
|
||||
|
||||
class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
|
||||
category_display = serializers.SerializerMethodField(label=_('Category display'))
|
||||
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
|
||||
type_display = serializers.SerializerMethodField(label=_('Type display'))
|
||||
|
||||
category_mapper = dict(const.AppCategory.choices)
|
||||
type_mapper = dict(const.AppType.choices)
|
||||
|
||||
class Meta:
|
||||
model = models.Account
|
||||
fields_mini = ['id', 'username', 'version']
|
||||
fields_write_only = ['password', 'private_key']
|
||||
fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
|
||||
fields = fields_mini + fields_fk + fields_write_only + [
|
||||
'type', 'type_display', 'category', 'category_display',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'username': {'default': '', 'required': False},
|
||||
'password': {'write_only': True},
|
||||
'app_display': {'label': _('Application display')},
|
||||
'systemuser_display': {'label': _('System User')}
|
||||
}
|
||||
use_model_bulk_create = True
|
||||
model_bulk_create_kwargs = {
|
||||
'ignore_conflicts': True
|
||||
}
|
||||
|
||||
def get_category_display(self, obj):
|
||||
return self.category_mapper.get(obj.category)
|
||||
|
||||
def get_type_display(self, obj):
|
||||
return self.type_mapper.get(obj.type)
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('systemuser', 'app')
|
||||
return queryset
|
||||
|
||||
def to_representation(self, instance):
|
||||
instance.load_auth()
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AppAccountSecretSerializer(AppAccountSerializer):
|
||||
class Meta(AppAccountSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
'public_key': {'write_only': False},
|
||||
'app_display': {'label': _('Application display')},
|
||||
'systemuser_display': {'label': _('System User')}
|
||||
}
|
||||
1
apps/applications/serializers/attrs/__init__.py
Normal file
1
apps/applications/serializers/attrs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .attrs import *
|
||||
@@ -0,0 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .db import *
|
||||
from .cloud import *
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['CloudSerializer']
|
||||
|
||||
|
||||
class CloudSerializer(serializers.Serializer):
|
||||
cluster = serializers.CharField(max_length=1024, label=_('Cluster'), allow_null=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['DBSerializer']
|
||||
|
||||
|
||||
class DBSerializer(serializers.Serializer):
|
||||
host = serializers.CharField(max_length=128, label=_('Host'), allow_null=True)
|
||||
port = serializers.IntegerField(label=_('Port'), allow_null=True)
|
||||
database = serializers.CharField(
|
||||
max_length=128, required=True, allow_null=True, label=_('Database')
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from common.utils import get_logger, is_uuid, get_object_or_none
|
||||
from assets.models import Asset
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
__all__ = ['RemoteAppSerializer']
|
||||
|
||||
|
||||
class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
instance = super().to_internal_value(data)
|
||||
return str(instance.id)
|
||||
|
||||
def to_representation(self, _id):
|
||||
# _id 是 instance.id
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(_id)
|
||||
# 解决删除资产后,远程应用更新页面会显示资产ID的问题
|
||||
asset = get_object_or_none(Asset, id=_id)
|
||||
if not asset:
|
||||
return None
|
||||
return _id
|
||||
|
||||
|
||||
class RemoteAppSerializer(serializers.Serializer):
|
||||
asset_info = serializers.SerializerMethodField()
|
||||
asset = ExistAssetPrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
|
||||
)
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), allow_null=True
|
||||
)
|
||||
|
||||
def validate_asset(self, asset):
|
||||
if not asset:
|
||||
raise serializers.ValidationError(_('This field is required.'))
|
||||
return asset
|
||||
|
||||
@staticmethod
|
||||
def get_asset_info(obj):
|
||||
asset_id = obj.get('asset')
|
||||
if not asset_id or not is_uuid(asset_id):
|
||||
return {}
|
||||
try:
|
||||
asset = Asset.objects.get(id=str(asset_id))
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.error(e)
|
||||
return {}
|
||||
if not asset:
|
||||
return {}
|
||||
asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
|
||||
return asset_info
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
from .mysql import *
|
||||
from .mariadb import *
|
||||
from .oracle import *
|
||||
from .pgsql import *
|
||||
|
||||
from .chrome import *
|
||||
from .mysql_workbench import *
|
||||
from .vmware_client import *
|
||||
from .custom import *
|
||||
|
||||
from .k8s import *
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['ChromeSerializer']
|
||||
|
||||
|
||||
class ChromeSerializer(RemoteAppSerializer):
|
||||
CHROME_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
||||
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
|
||||
)
|
||||
chrome_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
|
||||
)
|
||||
chrome_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'), allow_null=True,
|
||||
)
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['CustomSerializer']
|
||||
|
||||
|
||||
class CustomSerializer(RemoteAppSerializer):
|
||||
custom_cmdline = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Operating parameter'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target url'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..application_category import CloudSerializer
|
||||
|
||||
|
||||
__all__ = ['K8SSerializer']
|
||||
|
||||
|
||||
class K8SSerializer(CloudSerializer):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
from .mysql import MySQLSerializer
|
||||
|
||||
|
||||
__all__ = ['MariaDBSerializer']
|
||||
|
||||
|
||||
class MariaDBSerializer(MySQLSerializer):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['MySQLSerializer']
|
||||
|
||||
|
||||
class MySQLSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['MySQLWorkbenchSerializer']
|
||||
|
||||
|
||||
class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
||||
MYSQL_WORKBENCH_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
|
||||
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH,
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_ip = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('IP'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_port = serializers.IntegerField(
|
||||
required=False, label=_('Port'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_name = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Database'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['OracleSerializer']
|
||||
|
||||
|
||||
class OracleSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['PostgreSerializer']
|
||||
|
||||
|
||||
class PostgreSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['VMwareClientSerializer']
|
||||
|
||||
|
||||
class VMwareClientSerializer(RemoteAppSerializer):
|
||||
PATH = r'''
|
||||
C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient
|
||||
.exe
|
||||
'''
|
||||
VMWARE_CLIENT_PATH = ''.join(PATH.split())
|
||||
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH,
|
||||
allow_null=True
|
||||
)
|
||||
vmware_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True
|
||||
)
|
||||
42
apps/applications/serializers/attrs/attrs.py
Normal file
42
apps/applications/serializers/attrs/attrs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from rest_framework import serializers
|
||||
from applications import const
|
||||
from . import application_category, application_type
|
||||
|
||||
|
||||
__all__ = [
|
||||
'category_serializer_classes_mapping',
|
||||
'type_serializer_classes_mapping',
|
||||
'get_serializer_class_by_application_type',
|
||||
]
|
||||
|
||||
|
||||
# define `attrs` field `category serializers mapping`
|
||||
# ---------------------------------------------------
|
||||
|
||||
category_serializer_classes_mapping = {
|
||||
const.AppCategory.db.value: application_category.DBSerializer,
|
||||
const.AppCategory.remote_app.value: application_category.RemoteAppSerializer,
|
||||
const.AppCategory.cloud.value: application_category.CloudSerializer,
|
||||
}
|
||||
|
||||
# define `attrs` field `type serializers mapping`
|
||||
# -----------------------------------------------
|
||||
|
||||
type_serializer_classes_mapping = {
|
||||
# db
|
||||
const.AppType.mysql.value: application_type.MySQLSerializer,
|
||||
const.AppType.mariadb.value: application_type.MariaDBSerializer,
|
||||
const.AppType.oracle.value: application_type.OracleSerializer,
|
||||
const.AppType.pgsql.value: application_type.PostgreSerializer,
|
||||
# remote-app
|
||||
const.AppType.chrome.value: application_type.ChromeSerializer,
|
||||
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
|
||||
const.AppType.vmware_client.value: application_type.VMwareClientSerializer,
|
||||
const.AppType.custom.value: application_type.CustomSerializer,
|
||||
# cloud
|
||||
const.AppType.k8s.value: application_type.K8SSerializer
|
||||
}
|
||||
|
||||
|
||||
def get_serializer_class_by_application_type(_application_type):
|
||||
return type_serializer_classes_mapping.get(_application_type)
|
||||
@@ -1,26 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'DatabaseAppSerializer',
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DatabaseApp
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'type', 'get_type_display', 'host', 'port',
|
||||
'database', 'comment', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'date_updated'
|
||||
'get_type_display',
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'K8sAppSerializer',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.K8sApp
|
||||
fields = [
|
||||
'id', 'name', 'type', 'type_display', 'comment', 'created_by',
|
||||
'date_created', 'date_updated', 'cluster'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
@@ -1,86 +1,31 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import copy
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.fields.serializer import CustomMetaDictField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .. import const
|
||||
from ..models import RemoteApp
|
||||
from common.utils import get_logger
|
||||
from ..models import Application
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteAppSerializer', 'RemoteAppConnectionInfoSerializer',
|
||||
]
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class RemoteAppParamsDictField(CustomMetaDictField):
|
||||
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
|
||||
default_type = const.REMOTE_APP_TYPE_CHROME
|
||||
convert_key_remove_type_prefix = False
|
||||
convert_key_to_upper = False
|
||||
|
||||
|
||||
class RemoteAppSerializer(BulkOrgResourceModelSerializer):
|
||||
params = RemoteAppParamsDictField()
|
||||
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
|
||||
|
||||
class Meta:
|
||||
model = RemoteApp
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display',
|
||||
'path', 'params', 'date_created', 'created_by', 'comment',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'asset_info',
|
||||
'get_type_display'
|
||||
]
|
||||
|
||||
def process_params(self, instance, validated_data):
|
||||
new_params = copy.deepcopy(validated_data.get('params', {}))
|
||||
tp = validated_data.get('type', '')
|
||||
|
||||
if tp != instance.type:
|
||||
return new_params
|
||||
|
||||
old_params = instance.params
|
||||
fields = self.type_fields_map.get(instance.type, [])
|
||||
for field in fields:
|
||||
if not field.get('write_only', False):
|
||||
continue
|
||||
field_name = field['name']
|
||||
new_value = new_params.get(field_name, '')
|
||||
old_value = old_params.get(field_name, '')
|
||||
field_value = new_value if new_value else old_value
|
||||
new_params[field_name] = field_value
|
||||
|
||||
return new_params
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
params = self.process_params(instance, validated_data)
|
||||
validated_data['params'] = params
|
||||
return super().update(instance, validated_data)
|
||||
__all__ = ['RemoteAppConnectionInfoSerializer']
|
||||
|
||||
|
||||
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
|
||||
parameter_remote_app = serializers.SerializerMethodField()
|
||||
asset = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = RemoteApp
|
||||
model = Application
|
||||
fields = [
|
||||
'id', 'name', 'asset', 'parameter_remote_app',
|
||||
]
|
||||
read_only_fields = ['parameter_remote_app']
|
||||
|
||||
@staticmethod
|
||||
def get_asset(obj):
|
||||
return obj.attrs.get('asset')
|
||||
|
||||
@staticmethod
|
||||
def get_parameter_remote_app(obj):
|
||||
parameter = {
|
||||
'program': const.REMOTE_APP_BOOT_PROGRAM_NAME,
|
||||
'working_directory': '',
|
||||
'parameters': obj.parameters,
|
||||
}
|
||||
return parameter
|
||||
return obj.get_rdp_remote_app_setting()
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.urls import path, re_path
|
||||
from django.urls import path
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from common import api as capi
|
||||
from .. import api
|
||||
|
||||
|
||||
app_name = 'applications'
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
|
||||
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
|
||||
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
|
||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
|
||||
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
# path('accounts/', api.ApplicationAccountViewSet.as_view(), name='application-account'),
|
||||
# path('account-secrets/', api.ApplicationAccountSecretViewSet.as_view(), name='application-account-secret')
|
||||
]
|
||||
|
||||
old_version_urlpatterns = [
|
||||
re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api)
|
||||
]
|
||||
|
||||
urlpatterns += router.urls + old_version_urlpatterns
|
||||
urlpatterns += router.urls
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# coding:utf-8
|
||||
from django.urls import path
|
||||
|
||||
app_name = 'applications'
|
||||
|
||||
urlpatterns = [
|
||||
]
|
||||
@@ -1,11 +1,12 @@
|
||||
from .mixin import *
|
||||
from .admin_user import *
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .system_user import *
|
||||
from .system_user_relation import *
|
||||
from .accounts import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
|
||||
114
apps/assets/api/accounts.py
Normal file
114
apps/assets/api/accounts.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from django.db.models import F, Q
|
||||
from rest_framework.decorators import action
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
|
||||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
username = filters.CharFilter(method='do_nothing')
|
||||
ip = filters.CharFilter(field_name='ip', lookup_expr='exact')
|
||||
hostname = filters.CharFilter(field_name='hostname', lookup_expr='exact')
|
||||
node = filters.CharFilter(method='do_nothing')
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
qs = super().qs
|
||||
qs = self.filter_username(qs)
|
||||
qs = self.filter_node(qs)
|
||||
return qs
|
||||
|
||||
def filter_username(self, qs):
|
||||
username = self.get_query_param('username')
|
||||
if not username:
|
||||
return qs
|
||||
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
||||
return qs
|
||||
|
||||
def filter_node(self, qs):
|
||||
node_id = self.get_query_param('node')
|
||||
if not node_id:
|
||||
return qs
|
||||
node = get_object_or_404(Node, pk=node_id)
|
||||
node_ids = node.get_all_children(with_self=True).values_list('id', flat=True)
|
||||
node_ids = list(node_ids)
|
||||
qs = qs.filter(asset__nodes__in=node_ids)
|
||||
return qs
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields = [
|
||||
'asset', 'systemuser', 'id',
|
||||
]
|
||||
|
||||
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = AuthBook
|
||||
filterset_fields = ("username", "asset", "systemuser", 'ip', 'hostname')
|
||||
search_fields = ('username', 'ip', 'hostname', 'systemuser__username')
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
'verify_account': serializers.AssetTaskSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset() \
|
||||
.annotate(ip=F('asset__ip')) \
|
||||
.annotate(hostname=F('asset__hostname'))
|
||||
return queryset
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='verify')
|
||||
def verify_account(self, request, *args, **kwargs):
|
||||
account = super().get_object()
|
||||
task = test_accounts_connectivity_manual.delay([account])
|
||||
return Response(data={'task': task.id})
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSecretSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin, NeedMFAVerify)
|
||||
http_method_names = ['get']
|
||||
|
||||
|
||||
class AccountTaskCreateAPI(CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.AccountTaskSerializer
|
||||
filterset_fields = AccountViewSet.filterset_fields
|
||||
search_fields = AccountViewSet.search_fields
|
||||
filterset_class = AccountViewSet.filterset_class
|
||||
|
||||
def get_accounts(self):
|
||||
queryset = AuthBook.objects.all()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
accounts = self.get_accounts()
|
||||
task = test_accounts_connectivity_manual.delay(accounts)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
|
||||
return handler
|
||||
@@ -1,106 +1,30 @@
|
||||
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import AdminUser, Asset
|
||||
from ..models import SystemUser
|
||||
from .. import serializers
|
||||
from ..tasks import test_admin_user_connectivity_manual
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
|
||||
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
|
||||
'AdminUserAssetsListView',
|
||||
]
|
||||
__all__ = ['AdminUserViewSet']
|
||||
|
||||
|
||||
# 兼容一下老的 api
|
||||
class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
"""
|
||||
Admin user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = AdminUser
|
||||
filter_fields = ("name", "username")
|
||||
search_fields = filter_fields
|
||||
model = SystemUser
|
||||
filterset_fields = ("name", "username")
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = super().get_queryset().filter(type=SystemUser.Type.admin)
|
||||
queryset = queryset.annotate(assets_amount=Count('assets'))
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
has_related_asset = instance.assets.exists()
|
||||
if has_related_asset:
|
||||
data = {'msg': _('Deleted failed, There are related assets')}
|
||||
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AdminUserAuthApi(generics.UpdateAPIView):
|
||||
model = AdminUser
|
||||
serializer_class = serializers.AdminUserAuthSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
|
||||
model = AdminUser
|
||||
serializer_class = serializers.ReplaceNodeAdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
nodes = serializer.validated_data['nodes']
|
||||
assets = []
|
||||
for node in nodes:
|
||||
assets.extend([asset.id for asset in node.get_all_assets()])
|
||||
|
||||
with transaction.atomic():
|
||||
Asset.objects.filter(id__in=assets).update(admin_user=admin_user)
|
||||
|
||||
return Response({"msg": "ok"})
|
||||
else:
|
||||
return Response({'error': serializer.errors}, status=400)
|
||||
|
||||
|
||||
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
model = AdminUser
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
task = test_admin_user_connectivity_manual.delay(admin_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AdminUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filter_fields = ("hostname", "ip")
|
||||
http_method_names = ['get']
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(AdminUser, pk=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
admin_user = self.get_object()
|
||||
return admin_user.get_related_assets()
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.generics import RetrieveAPIView, ListAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from users.models import User, UserGroup
|
||||
from users.serializers import UserSerializer, UserGroupSerializer
|
||||
from users.filters import UserFilter
|
||||
from perms.models import AssetPermission
|
||||
from perms.serializers import AssetPermissionSerializer
|
||||
from perms.filters import AssetPermissionFilter
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual,
|
||||
test_system_users_connectivity_a_asset, push_system_users_a_asset
|
||||
)
|
||||
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||
|
||||
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet', 'AssetPlatformRetrieveApi',
|
||||
'AssetGatewayListApi', 'AssetPlatformViewSet',
|
||||
'AssetTaskCreateApi',
|
||||
'AssetTaskCreateApi', 'AssetsTaskCreateApi',
|
||||
'AssetPermUserListApi', 'AssetPermUserPermissionsListApi',
|
||||
'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi',
|
||||
]
|
||||
|
||||
|
||||
class AssetViewSet(OrgBulkModelViewSet):
|
||||
class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
model = Asset
|
||||
filter_fields = (
|
||||
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
|
||||
"is_active", 'ip'
|
||||
)
|
||||
filterset_fields = {
|
||||
'hostname': ['exact'],
|
||||
'ip': ['exact'],
|
||||
'system_users__id': ['exact'],
|
||||
'platform__base': ['exact'],
|
||||
'is_active': ['exact'],
|
||||
'protocols': ['exact', 'icontains']
|
||||
}
|
||||
search_fields = ("hostname", "ip")
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
ordering = ('hostname', )
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetSerializer,
|
||||
'display': serializers.AssetDisplaySerializer,
|
||||
'suggestion': serializers.MiniAssetSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
|
||||
def set_assets_node(self, assets):
|
||||
if not isinstance(assets, list):
|
||||
@@ -74,7 +89,7 @@ class AssetPlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filter_fields = ['name', 'base']
|
||||
filterset_fields = ['name', 'base']
|
||||
search_fields = ['name']
|
||||
|
||||
def get_permissions(self):
|
||||
@@ -90,32 +105,73 @@ class AssetPlatformViewSet(ModelViewSet):
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
|
||||
class AssetTaskCreateApi(generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
class AssetsTaskMixin:
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
instance = get_object_or_404(Asset, pk=pk)
|
||||
return instance
|
||||
def perform_assets_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
action = data['action']
|
||||
assets = data.get('assets', [])
|
||||
if action == "refresh":
|
||||
task = update_assets_hardware_info_manual.delay(assets)
|
||||
else:
|
||||
# action == 'test':
|
||||
task = test_assets_connectivity_manual.delay(assets)
|
||||
return task
|
||||
|
||||
def perform_create(self, serializer):
|
||||
asset = self.get_object()
|
||||
action = serializer.validated_data["action"]
|
||||
if action == "refresh":
|
||||
task = update_asset_hardware_info_manual.delay(asset)
|
||||
else:
|
||||
task = test_asset_connectivity_manual.delay(asset)
|
||||
task = self.perform_assets_task(serializer)
|
||||
self.set_task_to_serializer_data(serializer, task)
|
||||
|
||||
def set_task_to_serializer_data(self, serializer, task):
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
|
||||
|
||||
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
pk = self.kwargs.get('pk')
|
||||
request.data['asset'] = pk
|
||||
request.data['assets'] = [pk]
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def perform_asset_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
action = data['action']
|
||||
if action not in ['push_system_user', 'test_system_user']:
|
||||
return
|
||||
asset = data['asset']
|
||||
system_users = data.get('system_users')
|
||||
if not system_users:
|
||||
system_users = asset.get_all_systemusers()
|
||||
if action == 'push_system_user':
|
||||
task = push_system_users_a_asset.delay(system_users, asset=asset)
|
||||
elif action == 'test_system_user':
|
||||
task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset)
|
||||
else:
|
||||
task = None
|
||||
return task
|
||||
|
||||
def perform_create(self, serializer):
|
||||
task = self.perform_asset_task(serializer)
|
||||
if not task:
|
||||
task = self.perform_assets_task(serializer)
|
||||
self.set_task_to_serializer_data(serializer, task)
|
||||
|
||||
|
||||
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetsTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class AssetGatewayListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewayWithAuthSerializer
|
||||
model = Asset
|
||||
|
||||
def get_queryset(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
@@ -124,3 +180,102 @@ class AssetGatewayListApi(generics.ListAPIView):
|
||||
return []
|
||||
queryset = asset.domain.gateways.filter(protocol='ssh')
|
||||
return queryset
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
return asset
|
||||
|
||||
def get_asset_related_perms(self):
|
||||
asset = self.get_object()
|
||||
nodes = asset.get_all_nodes(flat=True)
|
||||
perms = AssetPermission.objects.filter(Q(assets=asset) | Q(nodes__in=nodes))
|
||||
return perms
|
||||
|
||||
|
||||
class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
filterset_class = UserFilter
|
||||
search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
users = User.objects.filter(
|
||||
Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms)
|
||||
).distinct()
|
||||
return users
|
||||
|
||||
|
||||
class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
serializer_class = UserGroupSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
user_groups = UserGroup.objects.filter(assetpermissions__in=perms).distinct()
|
||||
return user_groups
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = AssetPermission
|
||||
serializer_class = AssetPermissionSerializer
|
||||
filterset_class = AssetPermissionFilter
|
||||
search_fields = ('name',)
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
return asset
|
||||
|
||||
def filter_asset_related(self, queryset):
|
||||
asset = self.get_object()
|
||||
nodes = asset.get_all_nodes(flat=True)
|
||||
perms = queryset.filter(Q(assets=asset) | Q(nodes__in=nodes))
|
||||
return perms
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_asset_related(queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_user_related(queryset)
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
def filter_user_related(self, queryset):
|
||||
user = self.get_perm_user()
|
||||
user_groups = user.groups.all()
|
||||
perms = queryset.filter(Q(users=user) | Q(user_groups__in=user_groups))
|
||||
return perms
|
||||
|
||||
def get_perm_user(self):
|
||||
user_id = self.kwargs.get('perm_user_id')
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
return user
|
||||
|
||||
|
||||
class AssetPermUserGroupPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_user_group_related(queryset)
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
def filter_user_group_related(self, queryset):
|
||||
user_group = self.get_perm_user_group()
|
||||
perms = queryset.filter(user_groups=user_group)
|
||||
return perms
|
||||
|
||||
def get_perm_user_group(self):
|
||||
user_group_id = self.kwargs.get('perm_user_group_id')
|
||||
user_group = get_object_or_404(UserGroup, pk=user_group_id)
|
||||
return user_group
|
||||
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import coreapi
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics, filters
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
from common.mixins import CommonApiMixin
|
||||
from ..backends import AssetUserManager
|
||||
from ..models import Asset, Node, SystemUser
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
|
||||
]
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AssetUserFilterBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
kwargs = {}
|
||||
for field in view.filter_fields:
|
||||
value = request.GET.get(field)
|
||||
if not value:
|
||||
continue
|
||||
if field == "node_id":
|
||||
value = get_object_or_none(Node, pk=value)
|
||||
kwargs["node"] = value
|
||||
continue
|
||||
elif field == "asset_id":
|
||||
field = "asset"
|
||||
kwargs[field] = value
|
||||
if kwargs:
|
||||
queryset = queryset.filter(**kwargs)
|
||||
logger.debug("Filter {}".format(kwargs))
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserSearchBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
value = request.GET.get('search')
|
||||
if not value:
|
||||
return queryset
|
||||
queryset = queryset.search(value)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='latest', location='query', required=False,
|
||||
type='string', example='1',
|
||||
description='Only the latest version'
|
||||
)
|
||||
]
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
latest = request.GET.get('latest') == '1'
|
||||
if latest:
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetUserWriteSerializer,
|
||||
'display': serializers.AssetUserReadSerializer,
|
||||
'retrieve': serializers.AssetUserReadSerializer,
|
||||
}
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
filter_fields = [
|
||||
"id", "ip", "hostname", "username",
|
||||
"asset_id", "node_id",
|
||||
"prefer", "prefer_id",
|
||||
]
|
||||
search_fields = ["ip", "hostname", "username"]
|
||||
filter_backends = [
|
||||
AssetUserFilterBackend, AssetUserSearchBackend,
|
||||
AssetUserLatestFilterBackend,
|
||||
]
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
return False
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
if pk is None:
|
||||
return
|
||||
queryset = self.get_queryset()
|
||||
obj = queryset.get(id=pk)
|
||||
return obj
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
logger.error(e, exc_info=True)
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
manager = AssetUserManager()
|
||||
manager.delete(instance)
|
||||
|
||||
def get_queryset(self):
|
||||
manager = AssetUserManager()
|
||||
queryset = manager.all()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserAuthInfoViewSet(AssetUserViewSet):
|
||||
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
|
||||
http_method_names = ['get', 'post']
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class AssetUserTaskCreateAPI(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.AssetUserTaskSerializer
|
||||
filter_backends = AssetUserViewSet.filter_backends
|
||||
filter_fields = AssetUserViewSet.filter_fields
|
||||
|
||||
def get_asset_users(self):
|
||||
manager = AssetUserManager()
|
||||
queryset = manager.all()
|
||||
for cls in self.filter_backends:
|
||||
queryset = cls().filter_queryset(self.request, queryset, self)
|
||||
return list(queryset)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
asset_users = self.get_asset_users()
|
||||
# action = serializer.validated_data["action"]
|
||||
# only this
|
||||
# if action == "test":
|
||||
task = test_asset_users_connectivity_manual.delay(asset_users)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
@@ -1,29 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import reverse
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..hands import IsOrgAdmin
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
from .. import serializers
|
||||
|
||||
|
||||
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
|
||||
__all__ = [
|
||||
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
|
||||
'CommandConfirmStatusAPI'
|
||||
]
|
||||
|
||||
|
||||
class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilter
|
||||
filter_fields = ("name",)
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterSerializer
|
||||
|
||||
|
||||
class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilterRule
|
||||
filter_fields = ("content",)
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("content",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterRuleSerializer
|
||||
|
||||
@@ -35,3 +42,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
return cmd_filter.rules.all()
|
||||
|
||||
|
||||
class CommandConfirmAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.CommandConfirmSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
ticket = self.create_command_confirm_ticket()
|
||||
response_data = self.get_response_data(ticket)
|
||||
return Response(data=response_data, status=200)
|
||||
|
||||
def create_command_confirm_ticket(self):
|
||||
ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket(
|
||||
run_command=self.serializer.data.get('run_command'),
|
||||
session=self.serializer.session,
|
||||
cmd_filter_rule=self.serializer.cmd_filter_rule,
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
return ticket
|
||||
|
||||
@staticmethod
|
||||
def get_response_data(ticket):
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-assets:command-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
view_name='api-tickets:ticket-detail',
|
||||
kwargs={'pk': str(ticket.id)},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
return {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
'ticket_detail_url': ticket_detail_url,
|
||||
'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees]
|
||||
}
|
||||
|
||||
@lazyproperty
|
||||
def serializer(self):
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from rest_framework.views import APIView, Response
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.views import APIView, Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
@@ -16,10 +18,12 @@ __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
|
||||
|
||||
class DomainViewSet(OrgBulkModelViewSet):
|
||||
model = Domain
|
||||
filter_fields = ("name", )
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name", )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.query_params.get('gateway'):
|
||||
@@ -29,9 +33,9 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
|
||||
class GatewayViewSet(OrgBulkModelViewSet):
|
||||
model = Gateway
|
||||
filter_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
search_fields = ("domain__name", "name", "username", "ip")
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
|
||||
@@ -42,6 +46,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(Gateway.objects.all())
|
||||
local_port = self.request.data.get('port') or self.object.port
|
||||
try:
|
||||
local_port = int(local_port)
|
||||
except ValueError:
|
||||
raise ValidationError({'port': _('Number required')})
|
||||
ok, e = self.object.test_connective(local_port=local_port)
|
||||
if ok:
|
||||
return Response("ok")
|
||||
|
||||
@@ -13,7 +13,7 @@ __all__ = ['FavoriteAssetViewSet']
|
||||
class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteAssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filter_fields = ['asset']
|
||||
filterset_fields = ['asset']
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
|
||||
@@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet):
|
||||
permission_classes = [IsOrgAdmin]
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
search_fields = ['username', 'asset__ip', 'asset__hostname']
|
||||
|
||||
@@ -28,8 +28,8 @@ __all__ = ['LabelViewSet']
|
||||
|
||||
class LabelViewSet(OrgBulkModelViewSet):
|
||||
model = Label
|
||||
filter_fields = ("name", "value")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name", "value")
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LabelSerializer
|
||||
|
||||
|
||||
92
apps/assets/api/mixin.py
Normal file
92
apps/assets/api/mixin.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from typing import List
|
||||
|
||||
from common.utils.common import timeit
|
||||
from assets.models import Node, Asset
|
||||
from assets.pagination import NodeAssetTreePagination
|
||||
from common.utils import lazyproperty
|
||||
from assets.utils import get_node, is_query_node_all_assets
|
||||
|
||||
|
||||
class SerializeToTreeNodeMixin:
|
||||
|
||||
@timeit
|
||||
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
|
||||
if with_asset_amount:
|
||||
def _name(node: Node):
|
||||
return '{} ({})'.format(node.value, node.assets_amount)
|
||||
else:
|
||||
def _name(node: Node):
|
||||
return node.value
|
||||
data = [
|
||||
{
|
||||
'id': node.key,
|
||||
'name': _name(node),
|
||||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'open': node.is_org_root(),
|
||||
'meta': {
|
||||
'data': {
|
||||
"id": node.id,
|
||||
"key": node.key,
|
||||
"value": node.value,
|
||||
},
|
||||
'type': 'node'
|
||||
}
|
||||
}
|
||||
for node in nodes
|
||||
]
|
||||
return data
|
||||
|
||||
def get_platform(self, asset: Asset):
|
||||
default = 'file'
|
||||
icon = {'windows', 'linux'}
|
||||
platform = asset.platform_base.lower()
|
||||
if platform in icon:
|
||||
return platform
|
||||
return default
|
||||
|
||||
@timeit
|
||||
def serialize_assets(self, assets, node_key=None):
|
||||
if node_key is None:
|
||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||
else:
|
||||
get_pid = lambda asset: node_key
|
||||
|
||||
data = [
|
||||
{
|
||||
'id': str(asset.id),
|
||||
'name': asset.hostname,
|
||||
'title': asset.ip,
|
||||
'pId': get_pid(asset),
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': self.get_platform(asset),
|
||||
'chkDisabled': not asset.is_active,
|
||||
'meta': {
|
||||
'type': 'asset',
|
||||
'data': {
|
||||
'id': asset.id,
|
||||
'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'protocols': asset.protocols_as_list,
|
||||
'platform': asset.platform_base,
|
||||
'org_name': asset.org_name
|
||||
},
|
||||
}
|
||||
}
|
||||
for asset in assets
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
class FilterAssetByNodeMixin:
|
||||
pagination_class = NodeAssetTreePagination
|
||||
|
||||
@lazyproperty
|
||||
def is_query_node_all_assets(self):
|
||||
return is_query_node_all_assets(self.request)
|
||||
|
||||
@lazyproperty
|
||||
def node(self):
|
||||
return get_node(self.request)
|
||||
@@ -1,29 +1,40 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from functools import partial
|
||||
from collections import namedtuple, defaultdict
|
||||
|
||||
from collections import namedtuple
|
||||
from rest_framework import status
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404, Http404
|
||||
from django.db.models.signals import m2m_changed
|
||||
|
||||
from common.const.http import POST
|
||||
from common.exceptions import SomeoneIsDoingThis
|
||||
from common.const.signals import PRE_REMOVE, POST_REMOVE
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.tree import TreeNodeSerializer
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import current_org
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Node
|
||||
from ..tasks import (
|
||||
update_node_assets_hardware_info_manual,
|
||||
test_node_assets_connectivity_manual,
|
||||
check_node_assets_amount_task
|
||||
)
|
||||
from .. import serializers
|
||||
from .mixin import SerializeToTreeNodeMixin
|
||||
from assets.locks import NodeAddChildrenLock
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi',
|
||||
'NodeAddChildrenApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi',
|
||||
'NodeTaskCreateApi',
|
||||
@@ -32,7 +43,7 @@ __all__ = [
|
||||
|
||||
class NodeViewSet(OrgModelViewSet):
|
||||
model = Node
|
||||
filter_fields = ('value', 'key', 'id')
|
||||
filterset_fields = ('value', 'key', 'id')
|
||||
search_fields = ('value', )
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
@@ -43,6 +54,11 @@ class NodeViewSet(OrgModelViewSet):
|
||||
serializer.validated_data["key"] = child_key
|
||||
serializer.save()
|
||||
|
||||
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
|
||||
def check_assets_amount_task(self, request):
|
||||
task = check_node_assets_amount_task.delay(current_org.id)
|
||||
return Response(data={'task': task.id})
|
||||
|
||||
def perform_update(self, serializer):
|
||||
node = self.get_object()
|
||||
if node.is_org_root() and node.value != serializer.validated_data['value']:
|
||||
@@ -52,8 +68,11 @@ class NodeViewSet(OrgModelViewSet):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
node = self.get_object()
|
||||
if node.has_children_or_has_assets():
|
||||
error = _("Deletion failed and the node contains children or assets")
|
||||
if node.is_org_root():
|
||||
error = _("You can't delete the root node ({})".format(node.value))
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
if node.has_offspring_assets():
|
||||
error = _("Deletion failed and the node contains assets")
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
@@ -96,22 +115,27 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
_id = data.get("id")
|
||||
value = data.get("value")
|
||||
if not value:
|
||||
value = self.instance.get_next_child_preset_name()
|
||||
node = self.instance.create_child(value=value, _id=_id)
|
||||
# 避免查询 full value
|
||||
node._full_value = node.value
|
||||
serializer.instance = node
|
||||
with NodeAddChildrenLock(self.instance):
|
||||
data = serializer.validated_data
|
||||
_id = data.get("id")
|
||||
value = data.get("value")
|
||||
if not value:
|
||||
value = self.instance.get_next_child_preset_name()
|
||||
node = self.instance.create_child(value=value, _id=_id)
|
||||
# 避免查询 full value
|
||||
node._full_value = node.value
|
||||
serializer.instance = node
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
|
||||
key = self.request.query_params.get("key")
|
||||
|
||||
if not pk and not key:
|
||||
node = Node.org_root()
|
||||
self.is_initial = True
|
||||
if current_org.is_root():
|
||||
node = None
|
||||
else:
|
||||
node = Node.org_root()
|
||||
return node
|
||||
if pk:
|
||||
node = get_object_or_404(Node, pk=pk)
|
||||
@@ -119,16 +143,26 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
node = get_object_or_404(Node, key=key)
|
||||
return node
|
||||
|
||||
def get_org_root_queryset(self, query_all):
|
||||
if query_all:
|
||||
return Node.objects.all()
|
||||
else:
|
||||
return Node.org_root_nodes()
|
||||
|
||||
def get_queryset(self):
|
||||
query_all = self.request.query_params.get("all", "0") == "all"
|
||||
if not self.instance:
|
||||
return Node.objects.none()
|
||||
|
||||
if self.is_initial and current_org.is_root():
|
||||
return self.get_org_root_queryset(query_all)
|
||||
|
||||
if self.is_initial:
|
||||
with_self = True
|
||||
else:
|
||||
with_self = False
|
||||
|
||||
if not self.instance:
|
||||
return Node.objects.none()
|
||||
|
||||
if query_all:
|
||||
queryset = self.instance.get_all_children(with_self=with_self)
|
||||
else:
|
||||
@@ -136,7 +170,7 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
|
||||
class NodeChildrenAsTreeApi(NodeChildrenApi):
|
||||
class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
"""
|
||||
节点子节点作为树返回,
|
||||
[
|
||||
@@ -150,31 +184,23 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
|
||||
|
||||
"""
|
||||
model = Node
|
||||
serializer_class = TreeNodeSerializer
|
||||
http_method_names = ['get']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = [node.as_tree_node() for node in queryset]
|
||||
queryset = self.add_assets_if_need(queryset)
|
||||
queryset = sorted(queryset)
|
||||
return queryset
|
||||
def list(self, request, *args, **kwargs):
|
||||
nodes = self.get_queryset().order_by('value')
|
||||
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
|
||||
assets = self.get_assets()
|
||||
data = [*nodes, *assets]
|
||||
return Response(data=data)
|
||||
|
||||
def add_assets_if_need(self, queryset):
|
||||
def get_assets(self):
|
||||
include_assets = self.request.query_params.get('assets', '0') == '1'
|
||||
if not include_assets:
|
||||
return queryset
|
||||
if not self.instance or not include_assets:
|
||||
return []
|
||||
assets = self.instance.get_assets().only(
|
||||
"id", "hostname", "ip", "os",
|
||||
"org_id", "protocols",
|
||||
)
|
||||
for asset in assets:
|
||||
queryset.append(asset.as_tree_node(self.instance))
|
||||
return queryset
|
||||
|
||||
def check_need_refresh_nodes(self):
|
||||
if self.request.query_params.get('refresh', '0') == '1':
|
||||
Node.refresh_nodes()
|
||||
"id", "hostname", "ip", "os", "platform_id",
|
||||
"org_id", "protocols", "is_active",
|
||||
).prefetch_related('platform')
|
||||
return self.serialize_assets(assets, self.instance.key)
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
@@ -197,13 +223,12 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
serializer_class = serializers.NodeAddChildrenSerializer
|
||||
instance = None
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
def update(self, request, *args, **kwargs):
|
||||
""" 同时支持 put 和 patch 方法"""
|
||||
instance = self.get_object()
|
||||
nodes_id = request.data.get("nodes")
|
||||
children = [get_object_or_none(Node, id=pk) for pk in nodes_id]
|
||||
node_ids = request.data.get("nodes")
|
||||
children = Node.objects.filter(id__in=node_ids)
|
||||
for node in children:
|
||||
if not node:
|
||||
continue
|
||||
node.parent = instance
|
||||
return Response("OK")
|
||||
|
||||
@@ -228,15 +253,18 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
if instance != Node.org_root():
|
||||
instance.assets.remove(*tuple(assets))
|
||||
else:
|
||||
assets = [asset for asset in assets if asset.nodes.count() > 1]
|
||||
instance.assets.remove(*tuple(assets))
|
||||
node = self.get_object()
|
||||
node.assets.remove(*assets)
|
||||
|
||||
# 把孤儿资产添加到 root 节点
|
||||
orphan_assets = Asset.objects.filter(
|
||||
id__in=[a.id for a in assets],
|
||||
nodes__isnull=True
|
||||
).distinct()
|
||||
Node.org_root().assets.add(*orphan_assets)
|
||||
|
||||
|
||||
class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@@ -244,9 +272,39 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
for asset in assets:
|
||||
asset.nodes.set([instance])
|
||||
node = self.get_object()
|
||||
self.remove_old_nodes(assets)
|
||||
node.assets.add(*assets)
|
||||
|
||||
def remove_old_nodes(self, assets):
|
||||
m2m_model = Asset.nodes.through
|
||||
|
||||
# 查询资产与节点关系表,查出要移动资产与节点的所有关系
|
||||
relates = m2m_model.objects.filter(asset__in=assets).values_list('asset_id', 'node_id')
|
||||
if relates:
|
||||
# 对关系以资产进行分组,用来发 `reverse=False` 信号
|
||||
asset_nodes_mapper = defaultdict(set)
|
||||
for asset_id, node_id in relates:
|
||||
asset_nodes_mapper[asset_id].add(node_id)
|
||||
|
||||
# 组建一个资产 id -> Asset 的 mapper
|
||||
asset_mapper = {asset.id: asset for asset in assets}
|
||||
|
||||
# 创建删除关系信号发送函数
|
||||
senders = []
|
||||
for asset_id, node_id_set in asset_nodes_mapper.items():
|
||||
senders.append(partial(m2m_changed.send, sender=m2m_model, instance=asset_mapper[asset_id],
|
||||
reverse=False, model=Node, pk_set=node_id_set))
|
||||
# 发送 pre 信号
|
||||
[sender(action=PRE_REMOVE) for sender in senders]
|
||||
num = len(relates)
|
||||
asset_ids, node_ids = zip(*relates)
|
||||
# 删除之前的关系
|
||||
rows, _i = m2m_model.objects.filter(asset_id__in=asset_ids, node_id__in=node_ids).delete()
|
||||
if rows != num:
|
||||
raise SomeoneIsDoingThis
|
||||
# 发送 post 信号
|
||||
[sender(action=POST_REMOVE) for sender in senders]
|
||||
|
||||
|
||||
class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
@@ -267,7 +325,6 @@ class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
|
||||
@staticmethod
|
||||
def refresh_nodes_cache():
|
||||
Node.refresh_nodes()
|
||||
Task = namedtuple('Task', ['id'])
|
||||
task = Task(id="0")
|
||||
return task
|
||||
|
||||
@@ -3,40 +3,75 @@ from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import tmp_to_org
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from rest_framework.decorators import action
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
|
||||
from ..tasks import (
|
||||
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
|
||||
push_system_user_a_asset_manual,
|
||||
push_system_user_to_assets
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi',
|
||||
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
|
||||
'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi',
|
||||
]
|
||||
|
||||
|
||||
class SystemUserViewSet(OrgBulkModelViewSet):
|
||||
class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
System user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = SystemUser
|
||||
filter_fields = ("name", "username", "protocol")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'username': ['exact'],
|
||||
'protocol': ['exact', 'in'],
|
||||
'type': ['exact', 'in'],
|
||||
}
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.SystemUserSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.SystemUserSerializer,
|
||||
'list': serializers.SystemUserListSerializer,
|
||||
'suggestion': serializers.MiniSystemUserSerializer
|
||||
}
|
||||
ordering_fields = ('name', 'protocol', 'login_mode')
|
||||
ordering = ('name', )
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from')
|
||||
def su_from(self, request, *args, **kwargs):
|
||||
""" API 获取可选的 su_from 系统用户"""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = queryset.filter(
|
||||
protocol=SystemUser.Protocol.ssh, login_mode=SystemUser.LOGIN_AUTO
|
||||
)
|
||||
return self.get_paginate_response_if_need(queryset)
|
||||
|
||||
@action(methods=['get'], detail=True, url_path='su-to')
|
||||
def su_to(self, request, *args, **kwargs):
|
||||
""" 获取系统用户的所有 su_to 系统用户 """
|
||||
pk = kwargs.get('pk')
|
||||
system_user = get_object_or_404(SystemUser, pk=pk)
|
||||
queryset = system_user.su_to.all()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
return self.get_paginate_response_if_need(queryset)
|
||||
|
||||
def get_paginate_response_if_need(self, queryset):
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
@@ -52,6 +87,25 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
||||
model = SystemUser
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = SystemUserTempAuthSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = super().get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
pk = kwargs.get('pk')
|
||||
user = self.request.user
|
||||
data = serializer.validated_data
|
||||
instance_id = data.get('instance_id')
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(SystemUser, pk=pk)
|
||||
instance.set_temp_auth(instance_id, user.id, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
@@ -60,41 +114,48 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
asset_id = self.kwargs.get('asset_id')
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
username = self.request.query_params.get("username")
|
||||
instance.load_asset_more_auth(asset_id, username, user_id)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
username = instance.username
|
||||
if instance.username_same_with_user:
|
||||
username = self.request.query_params.get("username")
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
|
||||
with tmp_to_org(asset.org_id):
|
||||
instance.load_asset_special_auth(asset=asset, username=username)
|
||||
return instance
|
||||
app_id = self.kwargs.get('app_id')
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
instance.load_app_more_auth(app_id, user_id)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserTaskApi(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.SystemUserTaskSerializer
|
||||
|
||||
def do_push(self, system_user, asset=None):
|
||||
if asset is None:
|
||||
def do_push(self, system_user, asset_ids=None):
|
||||
if asset_ids is None:
|
||||
task = push_system_user_to_assets_manual.delay(system_user)
|
||||
else:
|
||||
username = self.request.query_params.get('username')
|
||||
task = push_system_user_a_asset_manual.delay(
|
||||
system_user, asset, username=username
|
||||
task = push_system_user_to_assets.delay(
|
||||
system_user.id, asset_ids, username=username
|
||||
)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def do_test(system_user, asset=None):
|
||||
task = test_system_user_connectivity_manual.delay(system_user)
|
||||
def do_test(system_user, asset_ids):
|
||||
task = test_system_user_connectivity_manual.delay(system_user, asset_ids)
|
||||
return task
|
||||
|
||||
def get_object(self):
|
||||
@@ -104,11 +165,20 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
asset = serializer.validated_data.get('asset')
|
||||
|
||||
if asset:
|
||||
assets = [asset]
|
||||
else:
|
||||
assets = serializer.validated_data.get('assets') or []
|
||||
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
asset_ids = asset_ids if asset_ids else None
|
||||
|
||||
system_user = self.get_object()
|
||||
if action == 'push':
|
||||
task = self.do_push(system_user, asset)
|
||||
task = self.do_push(system_user, asset_ids)
|
||||
else:
|
||||
task = self.do_test(system_user, asset)
|
||||
task = self.do_test(system_user, asset_ids)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
@@ -125,3 +195,18 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
pk = self.kwargs.get('pk', None)
|
||||
system_user = get_object_or_404(SystemUser, pk=pk)
|
||||
return system_user.cmd_filter_rules
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filterset_fields = ("hostname", "ip")
|
||||
search_fields = filterset_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(SystemUser, pk=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
system_user = self.get_object()
|
||||
return system_user.get_all_assets()
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
from django.db.models import F, Value
|
||||
from django.db.models import F, Value, Model
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
from .. import models, serializers
|
||||
|
||||
__all__ = [
|
||||
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
|
||||
'SystemUserUserRelationViewSet',
|
||||
'SystemUserUserRelationViewSet', 'BaseRelationViewSet',
|
||||
]
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RelationMixin:
|
||||
model: Model
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.model.objects.all()
|
||||
org_id = current_org.org_id()
|
||||
if org_id is not None:
|
||||
if not current_org.is_root():
|
||||
org_id = current_org.org_id()
|
||||
queryset = queryset.filter(systemuser__org_id=org_id)
|
||||
|
||||
queryset = queryset.annotate(systemuser_display=Concat(
|
||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
||||
Value(')')
|
||||
F('systemuser__name'), Value('('),
|
||||
F('systemuser__username'), Value(')')
|
||||
))
|
||||
return queryset
|
||||
|
||||
@@ -40,10 +46,11 @@ class RelationMixin:
|
||||
system_users_objects_map[i.systemuser].append(_id)
|
||||
|
||||
sender = self.get_sender()
|
||||
for system_user, objects in system_users_objects_map.items():
|
||||
for system_user, object_ids in system_users_objects_map.items():
|
||||
logger.debug('System user relation changed, send m2m_changed signals')
|
||||
m2m_changed.send(
|
||||
sender=sender, instance=system_user, action='post_add',
|
||||
reverse=False, model=model, pk_set=objects
|
||||
reverse=False, model=model, pk_set=set(object_ids)
|
||||
)
|
||||
|
||||
def get_sender(self):
|
||||
@@ -65,12 +72,12 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
"id", "asset__hostname", "asset__ip",
|
||||
"systemuser__name", "systemuser__username"
|
||||
"systemuser__name", "systemuser__username",
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
@@ -91,11 +98,11 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
'id', 'node', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
"node__value", "systemuser__name", "systemuser_username"
|
||||
"node__value", "systemuser__name", "systemuser__username"
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
@@ -112,7 +119,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserUserRelationSerializer
|
||||
model = models.SystemUser.users.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
'id', 'user', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
|
||||
def initial_some_nodes():
|
||||
from .models import Node
|
||||
Node.initial_some_nodes()
|
||||
|
||||
|
||||
def initial_some_nodes_callback(sender, **kwargs):
|
||||
initial_some_nodes()
|
||||
|
||||
|
||||
class AssetsConfig(AppConfig):
|
||||
@@ -19,7 +9,3 @@ class AssetsConfig(AppConfig):
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
try:
|
||||
initial_some_nodes()
|
||||
except Exception:
|
||||
post_migrate.connect(initial_some_nodes_callback, sender=self)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .manager import AssetUserManager
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user