mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
558 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb5b390d1d | ||
|
|
16c01733f1 | ||
|
|
b1b63445db | ||
|
|
fca15eae7f | ||
|
|
2c63b56f62 | ||
|
|
ea5e56b33e | ||
|
|
e4819ffe11 | ||
|
|
c34302325f | ||
|
|
3b5ee06535 | ||
|
|
8e5edfd179 | ||
|
|
d3355ab0ec | ||
|
|
81598a5264 | ||
|
|
298f6ba41d | ||
|
|
8e43e9ee2b | ||
|
|
adc8a8f7d3 | ||
|
|
1e3da50979 | ||
|
|
7ac385d64c | ||
|
|
2be74c4b84 | ||
|
|
75a72fb182 | ||
|
|
4c2274b14e | ||
|
|
a024f26768 | ||
|
|
2898c35970 | ||
|
|
62f5662bd0 | ||
|
|
0fe221019a | ||
|
|
d745314aa1 | ||
|
|
153fad9ac7 | ||
|
|
0792c7ec49 | ||
|
|
e617697553 | ||
|
|
9dc7da3595 | ||
|
|
f7f4d3a42e | ||
|
|
70fcbfe883 | ||
|
|
9e16b79abe | ||
|
|
8c839784fb | ||
|
|
10adb4e6b7 | ||
|
|
75c011f1c5 | ||
|
|
a882ca0d51 | ||
|
|
e0a2d03f44 | ||
|
|
2414f34a5a | ||
|
|
2aebfa51b2 | ||
|
|
f91bfedc50 | ||
|
|
68aad56bad | ||
|
|
556ce0a146 | ||
|
|
95f8b12912 | ||
|
|
25ae790f7d | ||
|
|
0464b1a9e6 | ||
|
|
3755f8f33a | ||
|
|
85b2ec2e6a | ||
|
|
9d1e94d3c2 | ||
|
|
be75edcb41 | ||
|
|
a5c6ba6cd6 | ||
|
|
81ef614820 | ||
|
|
c6949b4f68 | ||
|
|
a5acdb9f60 | ||
|
|
2366f02d10 | ||
|
|
dade0cadda | ||
|
|
e096244e75 | ||
|
|
3bc307d666 | ||
|
|
810c500402 | ||
|
|
6c0d0c3e92 | ||
|
|
af1150bb86 | ||
|
|
f7cbcc46f4 | ||
|
|
327c6beab4 | ||
|
|
196663f205 | ||
|
|
15423291cc | ||
|
|
021635b850 | ||
|
|
992c1407b6 | ||
|
|
1322106c91 | ||
|
|
42202bd528 | ||
|
|
b24d2f628a | ||
|
|
041302d5d2 | ||
|
|
a08dd5ee72 | ||
|
|
09ef72a4a8 | ||
|
|
26cf64ad2d | ||
|
|
0a04f0f351 | ||
|
|
1029556902 | ||
|
|
c41fc54380 | ||
|
|
c2fbe5c75a | ||
|
|
99e1b2cf92 | ||
|
|
33090c4cdf | ||
|
|
c8d7c7c56f | ||
|
|
aa7540045b | ||
|
|
e5f4b8000e | ||
|
|
44ffd09924 | ||
|
|
fe3059c1fd | ||
|
|
b76920a4bf | ||
|
|
b5ac5c5670 | ||
|
|
c3c0f87c01 | ||
|
|
d672122c79 | ||
|
|
0c71190337 | ||
|
|
14710e9c9e | ||
|
|
7eec50804c | ||
|
|
0fc5a33983 | ||
|
|
07779c5a7a | ||
|
|
d675b1d4fc | ||
|
|
514fa9cf0a | ||
|
|
2c73611cb4 | ||
|
|
83571718e9 | ||
|
|
521ec0245b | ||
|
|
e80b6936a2 | ||
|
|
2c4f937e0b | ||
|
|
2a5497de14 | ||
|
|
d87dc7cbd6 | ||
|
|
3b253e276c | ||
|
|
525538e775 | ||
|
|
2a8f8dd709 | ||
|
|
1e6e59d815 | ||
|
|
475678e29b | ||
|
|
7f52675bd3 | ||
|
|
6409b7deee | ||
|
|
4f37b2b920 | ||
|
|
c692eed3c6 | ||
|
|
dab8828b03 | ||
|
|
d692188a34 | ||
|
|
bc8df72603 | ||
|
|
bf466a1ba2 | ||
|
|
aff5b0035d | ||
|
|
b44fa64994 | ||
|
|
094446c548 | ||
|
|
64eda5f28b | ||
|
|
ab737ae09b | ||
|
|
55e04e8e9f | ||
|
|
5e70a8af15 | ||
|
|
031077c298 | ||
|
|
3f856e68f0 | ||
|
|
56862a965d | ||
|
|
e151548701 | ||
|
|
c56179e9e4 | ||
|
|
d23953932f | ||
|
|
2493647e5c | ||
|
|
00ed7bb025 | ||
|
|
b1aadf1ee9 | ||
|
|
86e6982383 | ||
|
|
dc42d1caa2 | ||
|
|
cb5d8fa13f | ||
|
|
3a3f7eaf71 | ||
|
|
9804ca5dd0 | ||
|
|
034d0e285c | ||
|
|
104d672634 | ||
|
|
529e3d12e0 | ||
|
|
978c1f6363 | ||
|
|
d25cde1bd5 | ||
|
|
a4be0ff2f3 | ||
|
|
a6d61721dd | ||
|
|
c3de7b78c2 | ||
|
|
e83d676712 | ||
|
|
63ee2dd8fb | ||
|
|
74f88d842d | ||
|
|
e61bae5ee4 | ||
|
|
b0b379e5a9 | ||
|
|
415521a003 | ||
|
|
c29d133776 | ||
|
|
d2dd487e2c | ||
|
|
f1bd4ea91f | ||
|
|
7647438792 | ||
|
|
015ff4b119 | ||
|
|
af9248ef7c | ||
|
|
c04ab1aab9 | ||
|
|
611a00a5fa | ||
|
|
57969a4e23 | ||
|
|
5f370c1c04 | ||
|
|
f026b86a20 | ||
|
|
0addba7c14 | ||
|
|
e4b0ab6a45 | ||
|
|
b4ac24ad6d | ||
|
|
500477fad1 | ||
|
|
3b9cb2a99c | ||
|
|
f8fade4cf2 | ||
|
|
be2708f83d | ||
|
|
516cb05d69 | ||
|
|
714b6b1233 | ||
|
|
3e3835dc28 | ||
|
|
f4ed4e1176 | ||
|
|
7b2d51f343 | ||
|
|
fe47e40588 | ||
|
|
4362f8d5af | ||
|
|
6f49d240af | ||
|
|
3eab621b28 | ||
|
|
afcbe60531 | ||
|
|
548a374c6d | ||
|
|
10c146b07d | ||
|
|
a647e73c02 | ||
|
|
7b02777f1e | ||
|
|
97e59384e0 | ||
|
|
70a07539af | ||
|
|
f98c170b8c | ||
|
|
0b94d7414a | ||
|
|
7aa0c9bf19 | ||
|
|
6d8e8856ac | ||
|
|
c240a471dc | ||
|
|
ea478fc801 | ||
|
|
5127214375 | ||
|
|
21c41a6334 | ||
|
|
b610d71e11 | ||
|
|
10b033010e | ||
|
|
c630b11bd5 | ||
|
|
b0f7c114fc | ||
|
|
72608146cc | ||
|
|
3213fe0984 | ||
|
|
f481463c64 | ||
|
|
4cf90df17c | ||
|
|
ffd98c6e3f | ||
|
|
1f8ded49fa | ||
|
|
7c7d7d52b2 | ||
|
|
f769d5a9bb | ||
|
|
c8758f417d | ||
|
|
ef36b2e662 | ||
|
|
fe8527fd07 | ||
|
|
2cb08b4785 | ||
|
|
a936092020 | ||
|
|
e602bc0341 | ||
|
|
3121b4e3ff | ||
|
|
eff562505e | ||
|
|
73cb5e10b4 | ||
|
|
c58d245636 | ||
|
|
e7af037513 | ||
|
|
54d1996507 | ||
|
|
71f8b40e21 | ||
|
|
59342a88c0 | ||
|
|
b8e6bc932b | ||
|
|
cddff9fd19 | ||
|
|
d856f1364a | ||
|
|
52709d2efa | ||
|
|
a20de3df16 | ||
|
|
e303b4f571 | ||
|
|
03fdaa03e4 | ||
|
|
b7b1d81ea0 | ||
|
|
e0fdfa52b9 | ||
|
|
8718dc6751 | ||
|
|
9e284f96e5 | ||
|
|
fc06295d04 | ||
|
|
9b73727bbc | ||
|
|
6bde31cdd0 | ||
|
|
2721793b8f | ||
|
|
2ec0cb8a2c | ||
|
|
d01d44b48d | ||
|
|
0ef7a9571c | ||
|
|
54fd1fb0c8 | ||
|
|
87c6eec619 | ||
|
|
e35fbfc7e9 | ||
|
|
3345456dc2 | ||
|
|
9ae74120ed | ||
|
|
9e5c132485 | ||
|
|
5cc2fdae4f | ||
|
|
e993f31b6d | ||
|
|
60edbb36a1 | ||
|
|
5da1ec55a7 | ||
|
|
b8c083af7e | ||
|
|
996621f303 | ||
|
|
ec9e5da653 | ||
|
|
d4e4015d91 | ||
|
|
005dd27701 | ||
|
|
ac6052546a | ||
|
|
0265adcc72 | ||
|
|
9654083662 | ||
|
|
08ff8fa285 | ||
|
|
f82f7eba2b | ||
|
|
a8cee26874 | ||
|
|
8080d36d90 | ||
|
|
3ed7477057 | ||
|
|
a3cddd5d34 | ||
|
|
26b3c60e5c | ||
|
|
b5dea38164 | ||
|
|
7addb881f6 | ||
|
|
9c395b674f | ||
|
|
b297ebe973 | ||
|
|
5c7bfcff1c | ||
|
|
76796f249d | ||
|
|
55a63477ed | ||
|
|
5942037d81 | ||
|
|
5882b8a682 | ||
|
|
34e75099a3 | ||
|
|
8fe84345e4 | ||
|
|
a31c3ccc30 | ||
|
|
e13e34098a | ||
|
|
e8653c74cd | ||
|
|
1433c35ff9 | ||
|
|
a237b5a63d | ||
|
|
2587c8693e | ||
|
|
dfe5e2bce3 | ||
|
|
91a34d1a88 | ||
|
|
1a05a942c2 | ||
|
|
30556023d1 | ||
|
|
aa022a02c1 | ||
|
|
433d829c29 | ||
|
|
3b507dc795 | ||
|
|
8233c69038 | ||
|
|
0fbc548c02 | ||
|
|
aa9ae14e46 | ||
|
|
04b35ba520 | ||
|
|
580d2cd80b | ||
|
|
da9136f7af | ||
|
|
1ce2706f20 | ||
|
|
bbdeba3659 | ||
|
|
3a26b9d102 | ||
|
|
ee757e261d | ||
|
|
f41e6db007 | ||
|
|
7eed7b32cc | ||
|
|
efb26132f6 | ||
|
|
572c5b6925 | ||
|
|
8a1cd7e2a9 | ||
|
|
c065f82d30 | ||
|
|
995c9a6c19 | ||
|
|
5ec970fab4 | ||
|
|
166745baf6 | ||
|
|
d320443c9f | ||
|
|
4bfa88f01f | ||
|
|
aedd8ba589 | ||
|
|
172b492bc3 | ||
|
|
c4280d259a | ||
|
|
b25ec559bb | ||
|
|
cd46c8c78e | ||
|
|
8839e6293b | ||
|
|
4f887b1b11 | ||
|
|
90840a4417 | ||
|
|
a18b9bad0a | ||
|
|
e1a238b778 | ||
|
|
ee44ae2e12 | ||
|
|
d77e84e6f8 | ||
|
|
2042c7a6e5 | ||
|
|
40aca26155 | ||
|
|
e18e76002c | ||
|
|
c77f02b295 | ||
|
|
3924ff0114 | ||
|
|
6a0264ad3b | ||
|
|
2d7349d596 | ||
|
|
c41a81c8d0 | ||
|
|
7ba19ab1a1 | ||
|
|
72247d1df3 | ||
|
|
faf82d7cfb | ||
|
|
4e8defc647 | ||
|
|
2f18208874 | ||
|
|
b37e8cdc3f | ||
|
|
5b960fc46b | ||
|
|
df51c82cfd | ||
|
|
e9deb6fc7a | ||
|
|
cca49fa9cd | ||
|
|
cfed849175 | ||
|
|
a7cc457f54 | ||
|
|
5996cedcd6 | ||
|
|
567c1b0124 | ||
|
|
2da541c127 | ||
|
|
794139782f | ||
|
|
307b739a03 | ||
|
|
ca5708988a | ||
|
|
90d84f4d69 | ||
|
|
758f418f63 | ||
|
|
a64ec8a1d2 | ||
|
|
60564d1b4f | ||
|
|
017710c056 | ||
|
|
a876a82a76 | ||
|
|
8423ae602f | ||
|
|
8e2471c1eb | ||
|
|
224a9fbdb3 | ||
|
|
797b184c7f | ||
|
|
b3632f6531 | ||
|
|
e3bc54e764 | ||
|
|
f0325c48df | ||
|
|
416d4bd0c3 | ||
|
|
10c877c120 | ||
|
|
f04378eaf8 | ||
|
|
b644c47173 | ||
|
|
45331dc9e8 | ||
|
|
8a565b9eef | ||
|
|
4eb7b50b52 | ||
|
|
fd64bd03b4 | ||
|
|
9c75147179 | ||
|
|
147e4cce94 | ||
|
|
d1e25e1fef | ||
|
|
af2ba07338 | ||
|
|
29b9adb684 | ||
|
|
64e0860d24 | ||
|
|
9934007397 | ||
|
|
4044a71aea | ||
|
|
9725f0c963 | ||
|
|
b017e68a56 | ||
|
|
9ca0eaf7ce | ||
|
|
94e60e180e | ||
|
|
8ed221ea5a | ||
|
|
42ebb1f82f | ||
|
|
9492518773 | ||
|
|
1cca9c10fb | ||
|
|
c4a6715eb8 | ||
|
|
4c31b5ec0f | ||
|
|
9fd7fa9339 | ||
|
|
a930f3aab3 | ||
|
|
5081fb5fe7 | ||
|
|
cb072123d6 | ||
|
|
761265dec5 | ||
|
|
89de111acc | ||
|
|
14327ee398 | ||
|
|
1b007c8c5c | ||
|
|
3222687aaa | ||
|
|
79994f5ddc | ||
|
|
8271492ec1 | ||
|
|
27560793f8 | ||
|
|
615929dd43 | ||
|
|
a1c1b128e9 | ||
|
|
fa2c70c6be | ||
|
|
46e119db1f | ||
|
|
0afff45bae | ||
|
|
31d219524b | ||
|
|
a20884e2ad | ||
|
|
eb6bddc599 | ||
|
|
8a8ed90eef | ||
|
|
75825f5baa | ||
|
|
0141fce27d | ||
|
|
3f9f9351f3 | ||
|
|
390b8693df | ||
|
|
dafc416783 | ||
|
|
04e46e4b1c | ||
|
|
ab1024fbf4 | ||
|
|
03afa4f974 | ||
|
|
edfca5eb24 | ||
|
|
02ca473492 | ||
|
|
484b75bb53 | ||
|
|
9c3fd59ef4 | ||
|
|
bbf3250161 | ||
|
|
8604b9019f | ||
|
|
966b4250b8 | ||
|
|
291f2b0e13 | ||
|
|
a1d15ef206 | ||
|
|
e76eec530f | ||
|
|
add4d8d2cd | ||
|
|
c6ece550a9 | ||
|
|
e3b620089a | ||
|
|
64f721875b | ||
|
|
02d3747c70 | ||
|
|
09494193ab | ||
|
|
702111f578 | ||
|
|
e08db7423f | ||
|
|
0c95faac04 | ||
|
|
534cbf1281 | ||
|
|
abe5fa9036 | ||
|
|
2a2f05e51c | ||
|
|
f460916e84 | ||
|
|
ad2cb233d7 | ||
|
|
0dbf035146 | ||
|
|
ea124fd0db | ||
|
|
83ff8dbf26 | ||
|
|
783c163324 | ||
|
|
3deced4ade | ||
|
|
63de4e1806 | ||
|
|
48d0c7b6cc | ||
|
|
20cc8a124f | ||
|
|
db050e405d | ||
|
|
e259d2a9e9 | ||
|
|
18c388f3a5 | ||
|
|
b088362ae3 | ||
|
|
c7f8ebb613 | ||
|
|
de2f9ae687 | ||
|
|
a806a2d3e6 | ||
|
|
7be76feeb0 | ||
|
|
f548abcb87 | ||
|
|
35c6b581e2 | ||
|
|
40b119786b | ||
|
|
ff9347e344 | ||
|
|
da0bbcee57 | ||
|
|
824d10ce93 | ||
|
|
1d6dcf9fb5 | ||
|
|
3325867f8f | ||
|
|
2f43aeee5d | ||
|
|
764d70ea4f | ||
|
|
ff6dbe67a6 | ||
|
|
7f36958683 | ||
|
|
fa5921cd86 | ||
|
|
540679df11 | ||
|
|
597b022905 | ||
|
|
ab34b9906e | ||
|
|
755fa8efa8 | ||
|
|
889713f00e | ||
|
|
c10436de47 | ||
|
|
333bb64b8b | ||
|
|
67a49dc5e9 | ||
|
|
8085db7acc | ||
|
|
6adeafd1d2 | ||
|
|
1faba95a48 | ||
|
|
ab07091eb8 | ||
|
|
f994f5d776 | ||
|
|
55fae1667d | ||
|
|
b3397c6aeb | ||
|
|
8ab4f6f004 | ||
|
|
5a37540eda | ||
|
|
1c1d507e34 | ||
|
|
9c40314edc | ||
|
|
acdde5a236 | ||
|
|
37a3566b0e | ||
|
|
a81f6bf97b | ||
|
|
b07f532a71 | ||
|
|
46270fd91c | ||
|
|
2b364c1476 | ||
|
|
9e47bf6ec5 | ||
|
|
89c7e514eb | ||
|
|
83c5344307 | ||
|
|
2beec1a03b | ||
|
|
d53925d1fb | ||
|
|
69d4a0a250 | ||
|
|
e33a67daf6 | ||
|
|
2036037675 | ||
|
|
0b16d70c0c | ||
|
|
8d6498c8f0 | ||
|
|
59d0f53725 | ||
|
|
518caa4f7a | ||
|
|
b8aaa7d249 | ||
|
|
c9f63a3f4a | ||
|
|
ec68fc9562 | ||
|
|
561ce53413 | ||
|
|
6bd597eadd | ||
|
|
005032ea00 | ||
|
|
de74477fd6 | ||
|
|
c43ad981bd | ||
|
|
088676d998 | ||
|
|
106bb9b63d | ||
|
|
451e61bb61 | ||
|
|
e1de210585 | ||
|
|
cce34f4939 | ||
|
|
def9bedd30 | ||
|
|
d2b3edffca | ||
|
|
aea2db84e8 | ||
|
|
6a4ffcc9ad | ||
|
|
4343b6487d | ||
|
|
ba695c4600 | ||
|
|
21a34ddc03 | ||
|
|
145c7952c9 | ||
|
|
43c4c78378 | ||
|
|
620834c817 | ||
|
|
fbd0b44d4f | ||
|
|
4b44a54d97 | ||
|
|
6716acb4ff | ||
|
|
35722a8466 | ||
|
|
e5659a1d07 | ||
|
|
414137cd73 | ||
|
|
1e3a15a3d0 | ||
|
|
dde7559f47 | ||
|
|
b0932e5137 | ||
|
|
5cdc4c3c28 | ||
|
|
25a2290804 | ||
|
|
4405abbedf | ||
|
|
3f6c9d519e | ||
|
|
bd84edea62 | ||
|
|
100bfe0304 | ||
|
|
11fd9a6567 | ||
|
|
90299a508c | ||
|
|
de9516dee5 | ||
|
|
db01a6d48d | ||
|
|
c2d271f00b | ||
|
|
90de229631 | ||
|
|
35a10fdd62 | ||
|
|
89cad224c5 | ||
|
|
9b2e1e08d8 | ||
|
|
aaaa87dd60 | ||
|
|
24cad76232 | ||
|
|
040f1fcbb8 | ||
|
|
d76ef788d2 | ||
|
|
38c870a0b5 | ||
|
|
ac9e7e9b40 | ||
|
|
5aed04d58b | ||
|
|
c05951b6c4 | ||
|
|
107215aadc |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
#### What this PR does / why we need it?
|
||||
|
||||
#### Summary of your change
|
||||
|
||||
#### Please indicate you've done the following:
|
||||
|
||||
- [ ] Made sure tests are passing and test coverage is added if needed.
|
||||
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
|
||||
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
|
||||
14
.github/workflows/release-drafter.yml
vendored
14
.github/workflows/release-drafter.yml
vendored
@@ -20,6 +20,8 @@ jobs:
|
||||
run: |
|
||||
TAG=$(basename ${GITHUB_REF})
|
||||
VERSION=${TAG/v/}
|
||||
wget https://raw.githubusercontent.com/jumpserver/installer/master/quick_start.sh
|
||||
sed -i "s@Version=.*@Version=v${VERSION}@g" quick_start.sh
|
||||
echo "::set-output name=TAG::$TAG"
|
||||
echo "::set-output name=VERSION::$VERSION"
|
||||
- name: Create Release
|
||||
@@ -31,6 +33,16 @@ jobs:
|
||||
config-name: release-config.yml
|
||||
version: ${{ steps.get_version.outputs.TAG }}
|
||||
tag: ${{ steps.get_version.outputs.TAG }}
|
||||
- name: Upload Quick Start Script
|
||||
id: upload-release-quick-start-shell
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: ./quick_start.sh
|
||||
asset_name: quick_start.sh
|
||||
asset_content_type: application/text
|
||||
|
||||
build-and-release:
|
||||
needs: create-realese
|
||||
@@ -43,4 +55,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-realese.outputs.upload_url }}
|
||||
upload_url: ${{ needs.create-realese.outputs.upload_url }}
|
||||
|
||||
23
.github/workflows/sync-gitee.yml
vendored
Normal file
23
.github/workflows/sync-gitee.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: 🔀 Sync mirror to Gitee
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
create:
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'jumpserver/jumpserver'
|
||||
steps:
|
||||
- name: mirror
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag')
|
||||
uses: wearerequired/git-mirror-action@v1
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}
|
||||
with:
|
||||
source-repo: 'git@github.com:jumpserver/jumpserver.git'
|
||||
destination-repo: 'git@gitee.com:jumpserver/jumpserver.git'
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
25
CONTRIBUTING.md
Normal file
25
CONTRIBUTING.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Contributing
|
||||
|
||||
## Create pull request
|
||||
PR are always welcome, even if they only contain small fixes like typos or a few lines of code. If there will be a significant effort, please document it as an issue and get a discussion going before starting to work on it.
|
||||
|
||||
Please submit a PR broken down into small changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in an incremental fashion.
|
||||
|
||||
This [development guideline](https://docs.jumpserver.org/zh/master/dev/rest_api/) contains information about repository structure, how to setup development environment, how to run it, and more.
|
||||
|
||||
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
|
||||
|
||||
## Report issues
|
||||
It is a great way to contribute by reporting an issue. Well-written and complete bug reports are always welcome! Please open an issue and follow the template to fill in required information.
|
||||
|
||||
Before opening any issue, please look up the existing issues to avoid submitting a duplication.
|
||||
If you find a match, you can "subscribe" to it to get notified on updates. If you have additional helpful information about the issue, please leave a comment.
|
||||
|
||||
When reporting issues, always include:
|
||||
|
||||
* Which version you are using.
|
||||
* Steps to reproduce the issue.
|
||||
* Snapshots or log files if needed
|
||||
|
||||
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
|
||||
replace those parts with "REDACTED" or other strings like "****".
|
||||
104
Dockerfile
104
Dockerfile
@@ -1,55 +1,91 @@
|
||||
# 编译代码
|
||||
FROM python:3.8.6-slim as stage-build
|
||||
FROM python:3.8-slim
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
# 构建运行时环境
|
||||
FROM python:3.8.6-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
ARG DEPENDENCIES=" \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev \
|
||||
sshpass"
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ARG TOOLS=" \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
locales \
|
||||
procps \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
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/* \
|
||||
&& apt update && sleep 1 && apt update \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& apt -y install ${TOOLS} \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
|
||||
COPY ./requirements/requirements.txt ./requirements/requirements.txt
|
||||
ARG TARGETARCH
|
||||
ARG ORACLE_LIB_MAJOR=19
|
||||
ARG ORACLE_LIB_MINOR=10
|
||||
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
|
||||
|
||||
RUN mkdir -p /opt/oracle/ \
|
||||
&& cd /opt/oracle/ \
|
||||
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \
|
||||
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \
|
||||
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \
|
||||
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f ${ORACLE_FILE}
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
|
||||
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 "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
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
|
||||
ADD . .
|
||||
RUN cd utils \
|
||||
&& bash -ixeu build.sh \
|
||||
&& mv ../release/jumpserver /opt/jumpserver \
|
||||
&& rm -rf /tmp/build \
|
||||
&& echo > /opt/jumpserver/config.yml
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
|
||||
63
README.md
63
README.md
@@ -1,10 +1,13 @@
|
||||
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
|
||||
<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/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
|
||||
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jumpserver/jumpserver.svg" /></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>
|
||||
|
||||
@@ -15,7 +18,7 @@
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
@@ -28,9 +31,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多租户: 一套系统,多个子公司或部门同时使用;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
### UI 展示
|
||||
@@ -55,12 +58,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- [手动安装](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)
|
||||
- [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 安装包 项目
|
||||
| 项目 | 状态 | 描述 |
|
||||
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer)| <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
### 社区
|
||||
|
||||
@@ -75,27 +81,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
感谢以下贡献者,让 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>
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
|
||||
### 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web 页面连接使用数据库,JumpServer Web 数据库依赖
|
||||
|
||||
|
||||
### JumpServer 企业版
|
||||
@@ -103,14 +95,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
### 案例研究
|
||||
|
||||
- [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)。
|
||||
- [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)
|
||||
|
||||
### 安全说明
|
||||
|
||||
@@ -124,11 +116,10 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
|
||||
|
||||
### License & Copyright
|
||||
|
||||
Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved.
|
||||
Copyright (c) 2014-2022 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ If you find a security problem, please contact us directly:
|
||||
- 400-052-0755
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2021 FIT2CLOUD Tech, Inc., All rights reserved.
|
||||
Copyright (c) 2014-2022 FIT2CLOUD Tech, Inc., All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
11
SECURITY.md
11
SECURITY.md
@@ -7,3 +7,14 @@ JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
|
||||
# Security Policy
|
||||
JumpServer is a security product, The installation and development should follow our security tips.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
All security bugs should be reported to the contact as below:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
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', ]
|
||||
__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()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
@@ -10,5 +9,4 @@ class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
||||
model = models.LoginAssetACL
|
||||
filterset_fields = ('name', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginAssetACLSerializer
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
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']
|
||||
__all__ = ['LoginAssetCheckAPI']
|
||||
|
||||
|
||||
class LoginAssetCheckAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.LoginAssetCheckSerializer
|
||||
model = LoginAssetACL
|
||||
rbac_perms = {
|
||||
'POST': 'tickets.add_superticket'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return LoginAssetACL.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
||||
@@ -46,7 +50,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-acls:login-asset-confirm-status',
|
||||
view_name='api-tickets:super-ticket-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
@@ -60,7 +64,8 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
'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]
|
||||
'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees],
|
||||
'ticket_id': str(ticket.id)
|
||||
}
|
||||
return data
|
||||
|
||||
@@ -70,6 +75,3 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AclsConfig(AppConfig):
|
||||
name = 'acls'
|
||||
verbose_name = _('Acls')
|
||||
|
||||
21
apps/acls/migrations/0003_auto_20211130_1037.py
Normal file
21
apps/acls/migrations/0003_auto_20211130_1037.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.13 on 2021-11-30 02:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('acls', '0002_auto_20210926_1047'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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'},
|
||||
),
|
||||
]
|
||||
@@ -123,6 +123,8 @@ class LoginACL(BaseACL):
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(self.user)
|
||||
applicant = self.user
|
||||
assignees = self.reviewers.all()
|
||||
ticket.create_process_map_and_node(assignees, applicant)
|
||||
ticket.open(applicant)
|
||||
return ticket
|
||||
|
||||
@@ -97,7 +97,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(assignees)
|
||||
ticket.create_process_map_and_node(assignees, user)
|
||||
ticket.open(applicant=user)
|
||||
return ticket
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .application import *
|
||||
from .account import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
|
||||
@@ -6,8 +6,11 @@ from django.db.models import F, Q
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from rbac.permissions import RBACPermission
|
||||
from assets.models import SystemUser
|
||||
from ..models import Account
|
||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||
from ..hands import NeedMFAVerify
|
||||
from .. import serializers
|
||||
|
||||
|
||||
@@ -31,7 +34,8 @@ class AccountFilterSet(BaseFilterSet):
|
||||
username = self.get_query_param('username')
|
||||
if not username:
|
||||
return qs
|
||||
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
||||
q = Q(username=username) | Q(systemuser__username=username)
|
||||
qs = qs.filter(q).distinct()
|
||||
return qs
|
||||
|
||||
|
||||
@@ -41,18 +45,21 @@ class ApplicationAccountViewSet(JMSBulkModelViewSet):
|
||||
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'))
|
||||
queryset = Account.get_queryset()
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
||||
perm_model = SystemUser
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
http_method_names = ['get', 'options']
|
||||
rbac_perms = {
|
||||
'retrieve': 'applications.view_applicationaccountsecret',
|
||||
'list': 'applications.view_applicationaccountsecret',
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
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
|
||||
|
||||
@@ -18,16 +17,19 @@ class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Application
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'category': ['exact'],
|
||||
'category': ['exact', 'in'],
|
||||
'type': ['exact', 'in'],
|
||||
}
|
||||
search_fields = ('name', 'type', 'category')
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': serializers.AppSerializer,
|
||||
'get_tree': TreeNodeSerializer,
|
||||
'suggestion': serializers.MiniAppSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
'get_tree': 'applications.view_application',
|
||||
'match': 'applications.match_application'
|
||||
}
|
||||
|
||||
@action(methods=['GET'], detail=False, url_path='tree')
|
||||
def get_tree(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
@@ -2,10 +2,8 @@
|
||||
#
|
||||
|
||||
from orgs.mixins import generics
|
||||
from ..hands import IsAppUser
|
||||
from .. import models
|
||||
from ..serializers import RemoteAppConnectionInfoSerializer
|
||||
from ..permissions import IsRemoteApp
|
||||
|
||||
|
||||
__all__ = [
|
||||
@@ -15,5 +13,4 @@ __all__ = [
|
||||
|
||||
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
|
||||
model = models.Application
|
||||
permission_classes = (IsAppUser, IsRemoteApp)
|
||||
serializer_class = RemoteAppConnectionInfoSerializer
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApplicationsConfig(AppConfig):
|
||||
name = 'applications'
|
||||
verbose_name = _('Applications')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from django.db.models import TextChoices
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AppCategory(TextChoices):
|
||||
class AppCategory(models.TextChoices):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
@@ -13,14 +13,20 @@ class AppCategory(TextChoices):
|
||||
def get_label(cls, category):
|
||||
return dict(cls.choices).get(category, '')
|
||||
|
||||
@classmethod
|
||||
def is_xpack(cls, category):
|
||||
return category in ['remote_app']
|
||||
|
||||
class AppType(TextChoices):
|
||||
|
||||
class AppType(models.TextChoices):
|
||||
# db category
|
||||
mysql = 'mysql', 'MySQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
pgsql = 'postgresql', 'PostgreSQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
|
||||
# remote-app category
|
||||
chrome = 'chrome', 'Chrome'
|
||||
@@ -34,8 +40,14 @@ class AppType(TextChoices):
|
||||
@classmethod
|
||||
def category_types_mapper(cls):
|
||||
return {
|
||||
AppCategory.db: [cls.mysql, cls.oracle, cls.pgsql, cls.mariadb, cls.sqlserver],
|
||||
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
|
||||
AppCategory.db: [
|
||||
cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
|
||||
cls.sqlserver, cls.redis, cls.mongodb
|
||||
],
|
||||
AppCategory.remote_app: [
|
||||
cls.chrome, cls.mysql_workbench,
|
||||
cls.vmware_client, cls.custom
|
||||
],
|
||||
AppCategory.cloud: [cls.k8s]
|
||||
}
|
||||
|
||||
@@ -63,6 +75,11 @@ class AppType(TextChoices):
|
||||
def cloud_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
|
||||
|
||||
@classmethod
|
||||
def is_xpack(cls, tp):
|
||||
tp_category_mapper = cls.type_category_mapper()
|
||||
category = tp_category_mapper[tp]
|
||||
|
||||
|
||||
|
||||
if AppCategory.is_xpack(category):
|
||||
return True
|
||||
return tp in ['oracle', 'postgresql', 'sqlserver']
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.permissions import NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.1.7 on 2019-05-20 11:04
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
|
||||
('path', models.CharField(max_length=128, verbose_name='App path')),
|
||||
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
|
||||
('params', common.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
|
||||
('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')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 3.1.12 on 2021-08-26 09:07
|
||||
|
||||
import assets.models.base
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
@@ -26,9 +26,9 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.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')),
|
||||
@@ -56,9 +56,9 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.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')),
|
||||
|
||||
18
apps/applications/migrations/0015_auto_20220112_2035.py
Normal file
18
apps/applications/migrations/0015_auto_20220112_2035.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-01-12 12:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0014_auto_20211105_1605'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
|
||||
),
|
||||
]
|
||||
24
apps/applications/migrations/0016_auto_20220118_1455.py
Normal file
24
apps/applications/migrations/0016_auto_20220118_1455.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.13 on 2022-01-18 06:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0015_auto_20220112_2035'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='app',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Application'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalaccount',
|
||||
name='app',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Application'),
|
||||
),
|
||||
]
|
||||
25
apps/applications/migrations/0017_auto_20220217_2135.py
Normal file
25
apps/applications/migrations/0017_auto_20220217_2135.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-17 13:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0016_auto_20220118_1455'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='account',
|
||||
options={'permissions': [('view_applicationaccountsecret', 'Can view application account secret'), ('change_appplicationaccountsecret', 'Can change application account secret')], 'verbose_name': 'Application account'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='applicationuser',
|
||||
options={'verbose_name': 'Application user'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='historicalaccount',
|
||||
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Application account'},
|
||||
),
|
||||
]
|
||||
18
apps/applications/migrations/0018_auto_20220223_1539.py
Normal file
18
apps/applications/migrations/0018_auto_20220223_1539.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-23 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0017_auto_20220217_2135'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
|
||||
),
|
||||
]
|
||||
17
apps/applications/migrations/0019_auto_20220310_1853.py
Normal file
17
apps/applications/migrations/0019_auto_20220310_1853.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-10 10:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0018_auto_20220223_1539'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='application',
|
||||
options={'ordering': ('name',), 'permissions': [('match_application', 'Can match application')], 'verbose_name': 'Application'},
|
||||
),
|
||||
]
|
||||
18
apps/applications/migrations/0020_auto_20220316_2028.py
Normal file
18
apps/applications/migrations/0020_auto_20220316_2028.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-16 12:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0019_auto_20220310_1853'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from simple_history.models import HistoricalRecords
|
||||
from django.db.models import F
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import lazyproperty
|
||||
@@ -7,16 +8,24 @@ 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"))
|
||||
app = models.ForeignKey(
|
||||
'applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Application')
|
||||
)
|
||||
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')
|
||||
verbose_name = _('Application account')
|
||||
unique_together = [('username', 'app', 'systemuser')]
|
||||
permissions = [
|
||||
('view_applicationaccountsecret', _('Can view application account secret')),
|
||||
('change_appplicationaccountsecret', _('Can change application account secret')),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -60,6 +69,10 @@ class Account(BaseUser):
|
||||
def type(self):
|
||||
return self.app.type
|
||||
|
||||
@lazyproperty
|
||||
def attrs(self):
|
||||
return self.app.attrs
|
||||
|
||||
@lazyproperty
|
||||
def app_display(self):
|
||||
return self.systemuser.name
|
||||
@@ -84,5 +97,14 @@ class Account(BaseUser):
|
||||
app = '*'
|
||||
return '{}@{}'.format(username, app)
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls):
|
||||
queryset = cls.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
|
||||
|
||||
def __str__(self):
|
||||
return self.smart_name
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urlencode, parse_qsl
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.tree import TreeNode
|
||||
from common.utils import is_uuid
|
||||
from assets.models import Asset, SystemUser
|
||||
|
||||
from ..utils import KubernetesTree
|
||||
from .. import const
|
||||
|
||||
|
||||
@@ -15,6 +20,14 @@ class ApplicationTreeNodeMixin:
|
||||
name: str
|
||||
type: str
|
||||
category: str
|
||||
attrs: dict
|
||||
|
||||
@staticmethod
|
||||
def create_tree_id(pid, type, v):
|
||||
i = dict(parse_qsl(pid))
|
||||
i[type] = v
|
||||
tree_id = urlencode(i)
|
||||
return tree_id
|
||||
|
||||
@classmethod
|
||||
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
|
||||
@@ -65,13 +78,15 @@ class ApplicationTreeNodeMixin:
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True):
|
||||
def create_category_tree_nodes(cls, pid, 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
|
||||
if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
|
||||
continue
|
||||
i = cls.create_tree_id(pid, 'category', category.value)
|
||||
node = cls.create_choice_node(
|
||||
category, i, pid=root_node.id, tp='category',
|
||||
category, i, pid=pid, tp='category',
|
||||
counts=counts, opened=False, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
@@ -81,17 +96,23 @@ class ApplicationTreeNodeMixin:
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True):
|
||||
def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
temp_pid = pid
|
||||
type_category_mapper = const.AppType.type_category_mapper()
|
||||
for tp in const.AppType.type_category_mapper().keys():
|
||||
types = const.AppType.type_category_mapper().keys()
|
||||
|
||||
for tp in types:
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
|
||||
continue
|
||||
category = type_category_mapper.get(tp)
|
||||
pid = root_node.id + '_' + category.value
|
||||
i = root_node.id + '_' + tp.value
|
||||
pid = cls.create_tree_id(pid, 'category', category.value)
|
||||
i = cls.create_tree_id(pid, 'type', tp.value)
|
||||
node = cls.create_choice_node(
|
||||
tp, i, pid, tp='type', counts=counts, opened=False,
|
||||
show_empty=show_empty, show_count=show_count
|
||||
)
|
||||
pid = temp_pid
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
@@ -109,40 +130,69 @@ class ApplicationTreeNodeMixin:
|
||||
return counts
|
||||
|
||||
@classmethod
|
||||
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
|
||||
def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True):
|
||||
counts = cls.get_tree_node_counts(queryset)
|
||||
tree_nodes = []
|
||||
|
||||
# 类别的节点
|
||||
tree_nodes += cls.create_category_tree_nodes(
|
||||
pid, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
# 类型的节点
|
||||
tree_nodes += cls.create_types_tree_nodes(
|
||||
pid, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
return tree_nodes
|
||||
|
||||
@classmethod
|
||||
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
|
||||
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
|
||||
tree_nodes += cls.create_category_type_tree_nodes(
|
||||
queryset, root_node.id, 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))
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
|
||||
continue
|
||||
node = app.as_tree_node(root_node.id)
|
||||
tree_nodes.append(node)
|
||||
return tree_nodes
|
||||
|
||||
def as_tree_node(self, pid):
|
||||
def create_app_tree_pid(self, root_id):
|
||||
pid = self.create_tree_id(root_id, 'category', self.category)
|
||||
pid = self.create_tree_id(pid, 'type', self.type)
|
||||
return pid
|
||||
|
||||
def as_tree_node(self, pid, k8s_as_tree=False):
|
||||
if self.type == const.AppType.k8s and k8s_as_tree:
|
||||
node = KubernetesTree(pid).as_tree_node(self)
|
||||
else:
|
||||
node = self._as_tree_node(pid)
|
||||
return node
|
||||
|
||||
def _attrs_to_tree(self):
|
||||
if self.category == const.AppCategory.db:
|
||||
return self.attrs
|
||||
return {}
|
||||
|
||||
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')
|
||||
pid = self.create_app_tree_pid(pid)
|
||||
node = TreeNode(**{
|
||||
'id': str(self.id),
|
||||
'name': self.name,
|
||||
@@ -156,6 +206,7 @@ class ApplicationTreeNodeMixin:
|
||||
'data': {
|
||||
'category': self.category,
|
||||
'type': self.type,
|
||||
'attrs': self._attrs_to_tree()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -183,6 +234,9 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
verbose_name = _('Application')
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name',)
|
||||
permissions = [
|
||||
('match_application', _('Can match application')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
category_display = self.get_category_display()
|
||||
@@ -193,6 +247,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
def category_remote_app(self):
|
||||
return self.category == const.AppCategory.remote_app.value
|
||||
|
||||
@property
|
||||
def category_cloud(self):
|
||||
return self.category == const.AppCategory.cloud.value
|
||||
|
||||
@property
|
||||
def category_db(self):
|
||||
return self.category == const.AppCategory.db.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:
|
||||
@@ -218,14 +280,26 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
'parameters': parameters
|
||||
}
|
||||
|
||||
def get_remote_app_asset(self):
|
||||
def get_remote_app_asset(self, raise_exception=True):
|
||||
asset_id = self.attrs.get('asset')
|
||||
if not asset_id:
|
||||
if is_uuid(asset_id):
|
||||
return Asset.objects.filter(id=asset_id).first()
|
||||
if raise_exception:
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
return asset
|
||||
|
||||
def get_target_ip(self):
|
||||
target_ip = ''
|
||||
if self.category_remote_app:
|
||||
asset = self.get_remote_app_asset()
|
||||
target_ip = asset.ip if asset else target_ip
|
||||
elif self.category_cloud:
|
||||
target_ip = self.attrs.get('cluster')
|
||||
elif self.category_db:
|
||||
target_ip = self.attrs.get('host')
|
||||
return target_ip
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = _('Application user')
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# 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 common.drf.serializers import MethodSerializer, SecretReadableMixin
|
||||
from .attrs import (
|
||||
category_serializer_classes_mapping,
|
||||
type_serializer_classes_mapping
|
||||
type_serializer_classes_mapping,
|
||||
type_secret_serializer_classes_mapping
|
||||
)
|
||||
from .. import models
|
||||
from .. import const
|
||||
@@ -23,17 +23,28 @@ __all__ = [
|
||||
class AppSerializerMixin(serializers.Serializer):
|
||||
attrs = MethodSerializer()
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
if isinstance(self.instance, models.Application):
|
||||
instance = self.instance
|
||||
else:
|
||||
instance = None
|
||||
return instance
|
||||
|
||||
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
|
||||
instance = self.app
|
||||
if instance:
|
||||
_type = instance.type
|
||||
_category = 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)
|
||||
if isinstance(self, AppAccountSecretSerializer):
|
||||
serializer_class = type_secret_serializer_classes_mapping.get(_type)
|
||||
else:
|
||||
serializer_class = type_serializer_classes_mapping.get(_type)
|
||||
elif _category:
|
||||
serializer_class = category_serializer_classes_mapping.get(_category)
|
||||
else:
|
||||
@@ -84,11 +95,13 @@ class MiniAppSerializer(serializers.ModelSerializer):
|
||||
fields = AppSerializer.Meta.fields_mini
|
||||
|
||||
|
||||
class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class AppAccountSerializer(AppSerializerMixin, 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'))
|
||||
date_created = serializers.DateTimeField(label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True)
|
||||
date_updated = serializers.DateTimeField(label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True)
|
||||
|
||||
category_mapper = dict(const.AppCategory.choices)
|
||||
type_mapper = dict(const.AppType.choices)
|
||||
@@ -96,22 +109,32 @@ class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = models.Account
|
||||
fields_mini = ['id', 'username', 'version']
|
||||
fields_write_only = ['password', 'private_key']
|
||||
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase']
|
||||
fields_other = ['date_created', 'date_updated']
|
||||
fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
|
||||
fields = fields_mini + fields_fk + fields_write_only + [
|
||||
'type', 'type_display', 'category', 'category_display',
|
||||
fields = fields_mini + fields_fk + fields_write_only + fields_other + [
|
||||
'type', 'type_display', 'category', 'category_display', 'attrs'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'username': {'default': '', 'required': False},
|
||||
'password': {'write_only': True},
|
||||
'app_display': {'label': _('Application display')},
|
||||
'systemuser_display': {'label': _('System User')}
|
||||
'systemuser_display': {'label': _('System User')},
|
||||
'account': {'label': _('account')}
|
||||
}
|
||||
use_model_bulk_create = True
|
||||
model_bulk_create_kwargs = {
|
||||
'ignore_conflicts': True
|
||||
}
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
if isinstance(self.instance, models.Account):
|
||||
instance = self.instance.app
|
||||
else:
|
||||
instance = None
|
||||
return instance
|
||||
|
||||
def get_category_display(self, obj):
|
||||
return self.category_mapper.get(obj.category)
|
||||
|
||||
@@ -129,8 +152,13 @@ class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AppAccountSecretSerializer(AppAccountSerializer):
|
||||
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
class Meta(AppAccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['CloudSerializer']
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from assets.models import Asset
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
__all__ = ['RemoteAppSerializer']
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ from .mariadb import *
|
||||
from .oracle import *
|
||||
from .pgsql import *
|
||||
from .sqlserver import *
|
||||
from .redis import *
|
||||
from .mongodb import *
|
||||
|
||||
from .chrome import *
|
||||
from .mysql_workbench import *
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['ChromeSerializer']
|
||||
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
|
||||
|
||||
|
||||
class ChromeSerializer(RemoteAppSerializer):
|
||||
@@ -14,13 +14,21 @@ class ChromeSerializer(RemoteAppSerializer):
|
||||
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,
|
||||
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,
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome username'), allow_null=True,
|
||||
)
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class ChromeSecretSerializer(ChromeSerializer):
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True, write_only=False
|
||||
)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['CustomSerializer']
|
||||
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
|
||||
|
||||
|
||||
class CustomSerializer(RemoteAppSerializer):
|
||||
@@ -18,10 +17,17 @@ class CustomSerializer(RemoteAppSerializer):
|
||||
allow_null=True,
|
||||
)
|
||||
custom_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class CustomSecretSerializer(RemoteAppSerializer):
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from ..application_category import CloudSerializer
|
||||
|
||||
|
||||
__all__ = ['K8SSerializer']
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from .mysql import MySQLSerializer
|
||||
|
||||
|
||||
__all__ = ['MariaDBSerializer']
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
__all__ = ['MongoDBSerializer']
|
||||
|
||||
|
||||
class MongoDBSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=27017, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -3,13 +3,9 @@ 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['MySQLWorkbenchSerializer']
|
||||
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
|
||||
|
||||
|
||||
class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
||||
@@ -27,10 +27,17 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench 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,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
@@ -3,10 +3,8 @@ 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)
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ 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,11 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
__all__ = ['RedisSerializer']
|
||||
|
||||
|
||||
class RedisSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=6379, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['SQLServerSerializer']
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['VMwareClientSerializer']
|
||||
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
|
||||
|
||||
|
||||
class VMwareClientSerializer(RemoteAppSerializer):
|
||||
@@ -23,10 +23,17 @@ class VMwareClientSerializer(RemoteAppSerializer):
|
||||
allow_null=True
|
||||
)
|
||||
vmware_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class VMwareClientSecretSerializer(RemoteAppSerializer):
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
import copy
|
||||
|
||||
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',
|
||||
'type_secret_serializer_classes_mapping'
|
||||
]
|
||||
|
||||
|
||||
# define `attrs` field `category serializers mapping`
|
||||
# ---------------------------------------------------
|
||||
|
||||
@@ -29,15 +29,34 @@ type_serializer_classes_mapping = {
|
||||
const.AppType.oracle.value: application_type.OracleSerializer,
|
||||
const.AppType.pgsql.value: application_type.PostgreSerializer,
|
||||
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
|
||||
# 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,
|
||||
const.AppType.redis.value: application_type.RedisSerializer,
|
||||
const.AppType.mongodb.value: application_type.MongoDBSerializer,
|
||||
# cloud
|
||||
const.AppType.k8s.value: application_type.K8SSerializer
|
||||
}
|
||||
|
||||
remote_app_serializer_classes_mapping = {
|
||||
# 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
|
||||
}
|
||||
|
||||
type_serializer_classes_mapping.update(remote_app_serializer_classes_mapping)
|
||||
|
||||
remote_app_secret_serializer_classes_mapping = {
|
||||
# remote-app
|
||||
const.AppType.chrome.value: application_type.ChromeSecretSerializer,
|
||||
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSecretSerializer,
|
||||
const.AppType.vmware_client.value: application_type.VMwareClientSecretSerializer,
|
||||
const.AppType.custom.value: application_type.CustomSecretSerializer
|
||||
}
|
||||
|
||||
type_secret_serializer_classes_mapping = copy.deepcopy(type_serializer_classes_mapping)
|
||||
|
||||
type_secret_serializer_classes_mapping.update(remote_app_secret_serializer_classes_mapping)
|
||||
|
||||
|
||||
def get_serializer_class_by_application_type(_application_type):
|
||||
return type_serializer_classes_mapping.get(_application_type)
|
||||
|
||||
@@ -11,6 +11,7 @@ app_name = 'applications'
|
||||
router = BulkRouter()
|
||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
|
||||
router.register(r'system-users-apps-relations', api.SystemUserAppRelationViewSet, 'system-users-apps-relation')
|
||||
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
|
||||
|
||||
|
||||
|
||||
4
apps/applications/utils/__init__.py
Normal file
4
apps/applications/utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .kubernetes_util import *
|
||||
186
apps/applications/utils/kubernetes_util.py
Normal file
186
apps/applications/utils/kubernetes_util.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from urllib3.exceptions import MaxRetryError
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from kubernetes.client import api_client
|
||||
from kubernetes.client.api import core_v1_api
|
||||
from kubernetes import client
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
|
||||
from rest_framework.generics import get_object_or_404
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.tree import TreeNode
|
||||
from assets.models import SystemUser
|
||||
|
||||
from .. import const
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class KubernetesClient:
|
||||
def __init__(self, url, token):
|
||||
self.url = url
|
||||
self.token = token
|
||||
|
||||
def get_api(self):
|
||||
configuration = client.Configuration()
|
||||
configuration.host = self.url
|
||||
configuration.verify_ssl = False
|
||||
configuration.api_key = {"authorization": "Bearer " + self.token}
|
||||
c = api_client.ApiClient(configuration=configuration)
|
||||
api = core_v1_api.CoreV1Api(c)
|
||||
return api
|
||||
|
||||
def get_namespace_list(self):
|
||||
api = self.get_api()
|
||||
namespace_list = []
|
||||
for ns in api.list_namespace().items:
|
||||
namespace_list.append(ns.metadata.name)
|
||||
return namespace_list
|
||||
|
||||
def get_services(self):
|
||||
api = self.get_api()
|
||||
ret = api.list_service_for_all_namespaces(watch=False)
|
||||
for i in ret.items:
|
||||
print("%s \t%s \t%s \t%s \t%s \n" % (
|
||||
i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports))
|
||||
|
||||
def get_pod_info(self, namespace, pod):
|
||||
api = self.get_api()
|
||||
resp = api.read_namespaced_pod(namespace=namespace, name=pod)
|
||||
return resp
|
||||
|
||||
def get_pod_logs(self, namespace, pod):
|
||||
api = self.get_api()
|
||||
log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200)
|
||||
return log_content
|
||||
|
||||
def get_pods(self):
|
||||
api = self.get_api()
|
||||
try:
|
||||
ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3))
|
||||
except MaxRetryError:
|
||||
logger.warning('Kubernetes connection timed out')
|
||||
return
|
||||
except ApiException as e:
|
||||
if e.status == 401:
|
||||
logger.warning('Kubernetes User not authenticated')
|
||||
else:
|
||||
logger.warning(e)
|
||||
return
|
||||
data = {}
|
||||
for i in ret.items:
|
||||
namespace = i.metadata.namespace
|
||||
pod_info = {
|
||||
'pod_name': i.metadata.name,
|
||||
'containers': [j.name for j in i.spec.containers]
|
||||
}
|
||||
if namespace in data:
|
||||
data[namespace].append(pod_info)
|
||||
else:
|
||||
data[namespace] = [pod_info, ]
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_kubernetes_data(app_id, system_user_id):
|
||||
from ..models import Application
|
||||
app = get_object_or_404(Application, id=app_id)
|
||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
||||
k8s = KubernetesClient(app.attrs['cluster'], system_user.token)
|
||||
return k8s.get_pods()
|
||||
|
||||
|
||||
class KubernetesTree:
|
||||
def __init__(self, tree_id):
|
||||
self.tree_id = tree_id
|
||||
|
||||
def as_tree_node(self, app):
|
||||
pid = app.create_app_tree_pid(self.tree_id)
|
||||
app_id = str(app.id)
|
||||
parent_info = {'app_id': app_id}
|
||||
node = self.create_tree_node(
|
||||
app_id, pid, app.name, 'k8s', parent_info
|
||||
)
|
||||
return node
|
||||
|
||||
def as_system_user_tree_node(self, system_user, parent_info):
|
||||
from ..models import ApplicationTreeNodeMixin
|
||||
system_user_id = str(system_user.id)
|
||||
username = system_user.username
|
||||
username = username if username else '*'
|
||||
name = f'{system_user.name}({username})'
|
||||
pid = urlencode({'app_id': self.tree_id})
|
||||
i = ApplicationTreeNodeMixin.create_tree_id(pid, 'system_user_id', system_user_id)
|
||||
parent_info.update({'system_user_id': system_user_id})
|
||||
node = self.create_tree_node(
|
||||
i, pid, name, 'system_user', parent_info, icon='user-tie'
|
||||
)
|
||||
return node
|
||||
|
||||
def as_namespace_pod_tree_node(self, name, meta, type, counts=0, is_container=False):
|
||||
from ..models import ApplicationTreeNodeMixin
|
||||
i = ApplicationTreeNodeMixin.create_tree_id(self.tree_id, type, name)
|
||||
meta.update({type: name})
|
||||
name = name if is_container else f'{name}({counts})'
|
||||
node = self.create_tree_node(
|
||||
i, self.tree_id, name, type, meta, icon='cloud', is_container=is_container
|
||||
)
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def create_tree_node(id_, pid, name, identity, parent_info, icon='', is_container=False):
|
||||
node = TreeNode(**{
|
||||
'id': id_,
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': pid,
|
||||
'isParent': not is_container,
|
||||
'open': False,
|
||||
'iconSkin': icon,
|
||||
'parentInfo': urlencode(parent_info),
|
||||
'meta': {
|
||||
'type': 'application',
|
||||
'data': {
|
||||
'category': const.AppCategory.cloud,
|
||||
'type': const.AppType.k8s,
|
||||
'identity': identity
|
||||
}
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
def async_tree_node(self, parent_info):
|
||||
pod_name = parent_info.get('pod')
|
||||
app_id = parent_info.get('app_id')
|
||||
namespace = parent_info.get('namespace')
|
||||
system_user_id = parent_info.get('system_user_id')
|
||||
|
||||
tree_nodes = []
|
||||
data = KubernetesClient.get_kubernetes_data(app_id, system_user_id)
|
||||
if not data:
|
||||
return tree_nodes
|
||||
|
||||
if pod_name:
|
||||
for container in next(
|
||||
filter(
|
||||
lambda x: x['pod_name'] == pod_name, data[namespace]
|
||||
)
|
||||
)['containers']:
|
||||
container_node = self.as_namespace_pod_tree_node(
|
||||
container, parent_info, 'container', is_container=True
|
||||
)
|
||||
tree_nodes.append(container_node)
|
||||
elif namespace:
|
||||
for pod in data[namespace]:
|
||||
pod_nodes = self.as_namespace_pod_tree_node(
|
||||
pod['pod_name'], parent_info, 'pod', len(pod['containers'])
|
||||
)
|
||||
tree_nodes.append(pod_nodes)
|
||||
elif system_user_id:
|
||||
for namespace, pods in data.items():
|
||||
namespace_node = self.as_namespace_pod_tree_node(
|
||||
namespace, parent_info, 'namespace', len(pods)
|
||||
)
|
||||
tree_nodes.append(namespace_node)
|
||||
return tree_nodes
|
||||
@@ -10,3 +10,4 @@ from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .account_backup import *
|
||||
|
||||
49
apps/assets/api/account_backup.py
Normal file
49
apps/assets/api/account_backup.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .. import serializers
|
||||
from ..tasks import execute_account_backup_plan
|
||||
from ..models import (
|
||||
AccountBackupPlan, AccountBackupPlanExecution
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet'
|
||||
]
|
||||
|
||||
|
||||
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
||||
model = AccountBackupPlan
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name',)
|
||||
serializer_class = serializers.AccountBackupPlanSerializer
|
||||
|
||||
|
||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||
search_fields = ('trigger',)
|
||||
filterset_fields = ('trigger', 'plan_id')
|
||||
http_method_names = ['get', 'post', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = AccountBackupPlanExecution.objects.all()
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
pid = serializer.data.get('plan')
|
||||
task = execute_account_backup_plan.delay(
|
||||
pid=pid, trigger=AccountBackupPlanExecution.Trigger.manual
|
||||
)
|
||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = queryset.order_by('-date_start')
|
||||
return queryset
|
||||
@@ -1,13 +1,15 @@
|
||||
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 django_filters import rest_framework as filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from rbac.permissions import RBACPermission
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from common.permissions import NeedMFAVerify
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
from .. import serializers
|
||||
@@ -26,6 +28,7 @@ class AccountFilterSet(BaseFilterSet):
|
||||
qs = super().qs
|
||||
qs = self.filter_username(qs)
|
||||
qs = self.filter_node(qs)
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
||||
def filter_username(self, qs):
|
||||
@@ -61,12 +64,13 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
'default': serializers.AccountSerializer,
|
||||
'verify_account': serializers.AssetTaskSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
rbac_perms = {
|
||||
'verify_account': 'assets.test_authbook',
|
||||
'partial_update': 'assets.change_assetaccountsecret',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset() \
|
||||
.annotate(ip=F('asset__ip')) \
|
||||
.annotate(hostname=F('asset__hostname'))
|
||||
queryset = AuthBook.get_queryset()
|
||||
return queryset
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='verify')
|
||||
@@ -76,24 +80,30 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
return Response(data={'task': task.id})
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountViewSet):
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSecretSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin, NeedMFAVerify)
|
||||
http_method_names = ['get']
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_assetaccountsecret',
|
||||
'retrieve': 'assets.view_assetaccountsecret',
|
||||
}
|
||||
|
||||
|
||||
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 check_permissions(self, request):
|
||||
return request.user.has_perm('assets.test_assetconnectivity')
|
||||
|
||||
def get_accounts(self):
|
||||
queryset = AuthBook.objects.all()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
@@ -110,5 +120,4 @@ class AccountTaskCreateAPI(CreateAPIView):
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
|
||||
return handler
|
||||
|
||||
@@ -2,9 +2,9 @@ from django.db.models import Count
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import SystemUser
|
||||
from .. import serializers
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -20,7 +20,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
filterset_fields = ("name", "username")
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = (RBACPermission,)
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
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
|
||||
@@ -17,7 +15,8 @@ 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 assets.api import FilterAssetByNodeMixin
|
||||
from ..models import Asset, Node, Platform, Gateway
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual,
|
||||
@@ -55,7 +54,9 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
|
||||
'default': serializers.AssetSerializer,
|
||||
'suggestion': serializers.MiniAssetSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
rbac_perms = {
|
||||
'match': 'assets.match_asset'
|
||||
}
|
||||
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
|
||||
def set_assets_node(self, assets):
|
||||
@@ -76,8 +77,10 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
|
||||
|
||||
class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'assets.view_gateway'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
asset_pk = self.kwargs.get('pk')
|
||||
@@ -87,16 +90,10 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
|
||||
class AssetPlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_fields = ['name', 'base']
|
||||
search_fields = ['name']
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method.lower() in ['get', 'options']:
|
||||
self.permission_classes = (IsOrgAdmin,)
|
||||
return super().get_permissions()
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
|
||||
self.permission_denied(
|
||||
@@ -131,7 +128,6 @@ class AssetsTaskMixin:
|
||||
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')
|
||||
@@ -139,11 +135,26 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
request.data['assets'] = [pk]
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
'push_system_user': 'assets.push_assetsystemuser',
|
||||
'test': 'assets.test_assetconnectivity',
|
||||
'test_system_user': 'assets.test_assetconnectivity'
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
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:
|
||||
@@ -166,24 +177,37 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetsTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
|
||||
class AssetGatewayListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewayWithAuthSerializer
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_gateway'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
if not asset.domain:
|
||||
return []
|
||||
return Gateway.objects.none()
|
||||
queryset = asset.domain.gateways.filter(protocol='ssh')
|
||||
return queryset
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
rbac_perms = {
|
||||
'GET': 'perms.view_assetpermission'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
@@ -201,6 +225,9 @@ class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
filterset_class = UserFilter
|
||||
search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
|
||||
serializer_class = UserSerializer
|
||||
rbac_perms = {
|
||||
'GET': 'perms.view_assetpermission'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
@@ -220,11 +247,13 @@ class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = AssetPermission
|
||||
serializer_class = AssetPermissionSerializer
|
||||
filterset_class = AssetPermissionFilter
|
||||
search_fields = ('name',)
|
||||
rbac_perms = {
|
||||
'list': 'perms.view_assetpermission'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
|
||||
@@ -8,14 +8,11 @@ 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 tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
from .. import serializers
|
||||
|
||||
__all__ = [
|
||||
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
|
||||
'CommandConfirmStatusAPI'
|
||||
]
|
||||
|
||||
|
||||
@@ -23,7 +20,6 @@ class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilter
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterSerializer
|
||||
|
||||
|
||||
@@ -31,7 +27,6 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilterRule
|
||||
filterset_fields = ('content',)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterRuleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -43,8 +38,10 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
|
||||
|
||||
class CommandConfirmAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.CommandConfirmSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'tickets.add_superticket'
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
ticket = self.create_command_confirm_ticket()
|
||||
@@ -56,14 +53,14 @@ class CommandConfirmAPI(CreateAPIView):
|
||||
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
|
||||
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',
|
||||
view_name='api-tickets:super-ticket-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
@@ -86,6 +83,3 @@ class CommandConfirmAPI(CreateAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..models import Domain, Gateway
|
||||
from .. import serializers
|
||||
@@ -20,7 +19,6 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
model = Domain
|
||||
filterset_fields = ("name", )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
@@ -35,13 +33,15 @@ class GatewayViewSet(OrgBulkModelViewSet):
|
||||
model = Gateway
|
||||
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
search_fields = ("domain__name", "name", "username", "ip")
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
|
||||
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
queryset = Gateway.objects.all()
|
||||
object = None
|
||||
rbac_perms = {
|
||||
'POST': 'assets.test_gateway'
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(Gateway.objects.all())
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
from assets.models import GatheredUser
|
||||
from common.permissions import IsOrgAdmin
|
||||
|
||||
from ..serializers import GatheredUserSerializer
|
||||
from ..filters import AssetRelatedByNodeFilterBackend
|
||||
@@ -15,7 +14,6 @@ __all__ = ['GatheredUserViewSet']
|
||||
class GatheredUserViewSet(OrgModelViewSet):
|
||||
model = GatheredUser
|
||||
serializer_class = GatheredUserSerializer
|
||||
permission_classes = [IsOrgAdmin]
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
|
||||
@@ -17,7 +17,6 @@ from django.db.models import Count
|
||||
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Label
|
||||
from .. import serializers
|
||||
|
||||
@@ -30,7 +29,6 @@ class LabelViewSet(OrgBulkModelViewSet):
|
||||
model = Label
|
||||
filterset_fields = ("name", "value")
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LabelSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
@@ -17,10 +17,9 @@ from common.mixins.api import SuggestionMixin
|
||||
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.api import OrgBulkModelViewSet
|
||||
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,
|
||||
@@ -31,7 +30,6 @@ from .. import serializers
|
||||
from .mixin import SerializeToTreeNodeMixin
|
||||
from assets.locks import NodeAddChildrenLock
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
@@ -42,18 +40,15 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class NodeViewSet(SuggestionMixin, OrgModelViewSet):
|
||||
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Node
|
||||
filterset_fields = ('value', 'key', 'id')
|
||||
search_fields = ('value', )
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
search_fields = ('value',)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
|
||||
# 仅支持根节点指直接创建,子节点下的节点需要通过children接口创建
|
||||
def perform_create(self, serializer):
|
||||
child_key = Node.org_root().get_next_child_key()
|
||||
serializer.validated_data["key"] = child_key
|
||||
serializer.save()
|
||||
rbac_perms = {
|
||||
'match': 'assets.match_node',
|
||||
'check_assets_amount_task': 'assets.change_node'
|
||||
}
|
||||
|
||||
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
|
||||
def check_assets_amount_task(self, request):
|
||||
@@ -91,7 +86,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
]
|
||||
"""
|
||||
model = Node
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = TreeNodeSerializer
|
||||
|
||||
@staticmethod
|
||||
@@ -106,7 +100,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
|
||||
|
||||
class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
instance = None
|
||||
is_initial = False
|
||||
@@ -205,7 +198,6 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -220,7 +212,6 @@ class NodeAssetsApi(generics.ListAPIView):
|
||||
|
||||
class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeAddChildrenSerializer
|
||||
instance = None
|
||||
|
||||
@@ -237,7 +228,6 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -249,7 +239,6 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -268,7 +257,6 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -309,9 +297,21 @@ class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
|
||||
|
||||
class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
perm_model = Asset
|
||||
model = Node
|
||||
serializer_class = serializers.NodeTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
'test': 'assets.test_assetconnectivity'
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
def get_object(self):
|
||||
node_id = self.kwargs.get('pk')
|
||||
@@ -344,4 +344,3 @@ class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
else:
|
||||
task = test_node_assets_connectivity_manual.delay(node)
|
||||
self.set_serializer_data(serializer, task)
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Q
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from rest_framework.decorators import action
|
||||
from users.models import User, UserGroup
|
||||
from applications.models import Application
|
||||
from ..models import SystemUser, Asset, CommandFilter, CommandFilterRule
|
||||
from ..models import SystemUser, CommandFilterRule
|
||||
from .. import serializers
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
|
||||
from ..tasks import (
|
||||
@@ -47,7 +44,11 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
}
|
||||
ordering_fields = ('name', 'protocol', 'login_mode')
|
||||
ordering = ('name', )
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
rbac_perms = {
|
||||
'su_from': 'assets.view_systemuser',
|
||||
'su_to': 'assets.view_systemuser',
|
||||
'match': 'assets.match_systemuser'
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from')
|
||||
def su_from(self, request, *args, **kwargs):
|
||||
@@ -81,8 +82,13 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
Get system user auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'assets.view_systemusersecret',
|
||||
'list': 'assets.view_systemusersecret',
|
||||
'change': 'assets.change_systemuser',
|
||||
'destroy': 'assets.change_systemuser',
|
||||
}
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -98,14 +104,14 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
||||
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')
|
||||
asset_or_app_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)
|
||||
instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
@@ -114,7 +120,6 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_object(self):
|
||||
@@ -131,8 +136,10 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'assets.view_systemusersecret',
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
@@ -144,7 +151,6 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
||||
|
||||
|
||||
class SystemUserTaskApi(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.SystemUserTaskSerializer
|
||||
|
||||
def do_push(self, system_user, asset_ids=None):
|
||||
@@ -166,6 +172,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(SystemUser, pk=pk)
|
||||
|
||||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'push': 'assets.push_assetsystemuser',
|
||||
'test': 'assets.test_assetconnectivity'
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
asset = serializer.validated_data.get('asset')
|
||||
@@ -189,56 +207,40 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
|
||||
|
||||
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_commandfilterule'
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
from ..serializers import CommandFilterRuleSerializer
|
||||
return CommandFilterRuleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user_groups = []
|
||||
user_id = self.request.query_params.get('user_id')
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if user:
|
||||
user_groups.extend(list(user.groups.all()))
|
||||
user_group_id = self.request.query_params.get('user_group_id')
|
||||
user_group = get_object_or_none(UserGroup, pk=user_group_id)
|
||||
if user_group:
|
||||
user_groups.append(user_group)
|
||||
system_user_id = self.kwargs.get('pk', None)
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
if not system_user:
|
||||
system_user_id = self.request.query_params.get('system_user_id')
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
asset_id = self.request.query_params.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
application_id = self.request.query_params.get('application_id')
|
||||
application = get_object_or_none(Application, pk=application_id)
|
||||
q = Q()
|
||||
if user:
|
||||
q |= Q(users=user)
|
||||
if user_groups:
|
||||
q |= Q(user_groups__in=set(user_groups))
|
||||
if system_user:
|
||||
q |= Q(system_users=system_user)
|
||||
if asset:
|
||||
q |= Q(assets=asset)
|
||||
if application:
|
||||
q |= Q(applications=application)
|
||||
if q:
|
||||
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
|
||||
rule_ids = cmd_filters.values_list('rules', flat=True)
|
||||
rules = CommandFilterRule.objects.filter(id__in=rule_ids)
|
||||
else:
|
||||
rules = CommandFilterRule.objects.none()
|
||||
rules = CommandFilterRule.get_queryset(
|
||||
user_id=user_id,
|
||||
user_group_id=user_group_id,
|
||||
system_user_id=system_user_id,
|
||||
asset_id=asset_id,
|
||||
application_id=application_id
|
||||
)
|
||||
return rules
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filterset_fields = ("hostname", "ip")
|
||||
search_fields = filterset_fields
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_asset'
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -65,13 +64,13 @@ class RelationMixin:
|
||||
|
||||
|
||||
class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
|
||||
pass
|
||||
perm_model = models.SystemUser
|
||||
|
||||
|
||||
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
perm_model = models.AuthBook
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
@@ -97,7 +96,6 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
'id', 'node', 'systemuser',
|
||||
]
|
||||
@@ -118,7 +116,6 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserUserRelationSerializer
|
||||
model = models.SystemUser.users.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
'id', 'user', 'systemuser',
|
||||
]
|
||||
@@ -140,4 +137,3 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AssetsConfig(AppConfig):
|
||||
name = 'assets'
|
||||
verbose_name = _('App assets')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
|
||||
@@ -11,5 +11,4 @@
|
||||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
|
||||
from users.models import User, UserGroup
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 2.1.7 on 2019-06-24 13:08
|
||||
|
||||
import assets.models.utils
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -15,61 +15,61 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.1.7 on 2019-07-11 12:18
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -14,21 +14,21 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-06 07:26
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
|
||||
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
|
||||
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
|
||||
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('meta', common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('internal', models.BooleanField(default=False, verbose_name='Internal')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-05 16:10
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
@@ -58,9 +58,9 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.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')),
|
||||
|
||||
62
apps/assets/migrations/0084_auto_20220112_1959.py
Normal file
62
apps/assets/migrations/0084_auto_20220112_1959.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Generated by Django 3.1.13 on 2022-01-12 11:59
|
||||
|
||||
import common.db.encoder
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0083_auto_20211215_1436'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccountBackupPlan',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('is_periodic', models.BooleanField(default=False)),
|
||||
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')),
|
||||
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')),
|
||||
('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')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('types', models.IntegerField(choices=[(255, 'All'), (1, 'Asset'), (2, 'Application')], default=255, verbose_name='Type')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('recipients', models.ManyToManyField(blank=True, related_name='recipient_escape_route_plans', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account backup plan',
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('name', 'org_id')},
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AccountBackupPlanExecution',
|
||||
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)),
|
||||
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
|
||||
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
|
||||
('plan_snapshot', models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, verbose_name='Account backup snapshot')),
|
||||
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')),
|
||||
('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')),
|
||||
('is_success', models.BooleanField(default=False, verbose_name='Is success')),
|
||||
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', to='assets.accountbackupplan', verbose_name='Account backup plan')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account backup execution',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
apps/assets/migrations/0085_commandfilterrule_ignore_case.py
Normal file
18
apps/assets/migrations/0085_commandfilterrule_ignore_case.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-08 02:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0084_auto_20220112_1959'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='commandfilterrule',
|
||||
name='ignore_case',
|
||||
field=models.BooleanField(default=True, verbose_name='Ignore case'),
|
||||
),
|
||||
]
|
||||
25
apps/assets/migrations/0086_auto_20220217_2135.py
Normal file
25
apps/assets/migrations/0086_auto_20220217_2135.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-17 13:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0085_commandfilterrule_ignore_case'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'permissions': [('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='authbook',
|
||||
options={'permissions': [('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='label',
|
||||
options={'verbose_name': 'Label'},
|
||||
),
|
||||
]
|
||||
18
apps/assets/migrations/0087_auto_20220223_1539.py
Normal file
18
apps/assets/migrations/0087_auto_20220223_1539.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2022-02-23 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0086_auto_20220217_2135'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
25
apps/assets/migrations/0088_auto_20220303_1612.py
Normal file
25
apps/assets/migrations/0088_auto_20220303_1612.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-03 08:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0087_auto_20220223_1539'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='node',
|
||||
options={'ordering': ['parent_key', 'value'], 'permissions': [('match_node', 'Can match node')], 'verbose_name': 'Node'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='systemuser',
|
||||
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
|
||||
),
|
||||
]
|
||||
29
apps/assets/migrations/0089_auto_20220310_0616.py
Normal file
29
apps/assets/migrations/0089_auto_20220310_0616.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.1.14 on 2022-03-09 22:16
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0088_auto_20220303_1612'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='authbook',
|
||||
options={'permissions': [('test_authbook', 'Can test asset account connectivity'), ('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='systemuser',
|
||||
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='gateway',
|
||||
options={'permissions': [('test_gateway', 'Test gateway')], 'verbose_name': 'Gateway'},
|
||||
),
|
||||
]
|
||||
32
apps/assets/migrations/0090_auto_20220412_1145.py
Normal file
32
apps/assets/migrations/0090_auto_20220412_1145.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.1.14 on 2022-04-12 03:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def create_internal_platform(apps, schema_editor):
|
||||
model = apps.get_model("assets", "Platform")
|
||||
db_alias = schema_editor.connection.alias
|
||||
type_platforms = (
|
||||
('AIX', 'Unix', None),
|
||||
)
|
||||
for name, base, meta in type_platforms:
|
||||
defaults = {'name': name, 'base': base, 'meta': meta, 'internal': True}
|
||||
model.objects.using(db_alias).update_or_create(
|
||||
name=name, defaults=defaults
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0089_auto_20220310_0616'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='number',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
|
||||
),
|
||||
migrations.RunPython(create_internal_platform)
|
||||
]
|
||||
@@ -12,3 +12,4 @@ from .utils import *
|
||||
from .authbook import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .backup import *
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
#
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
@@ -8,11 +8,10 @@ from functools import reduce
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.db import models
|
||||
from common.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from common.fields.model import JsonDictTextField
|
||||
from common.db.fields import JsonDictTextField
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
|
||||
@@ -59,7 +58,7 @@ class AssetQuerySet(models.QuerySet):
|
||||
class ProtocolsMixin:
|
||||
protocols = ''
|
||||
|
||||
class Protocol(TextChoices):
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
@@ -224,7 +223,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
||||
|
||||
# Some information
|
||||
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
|
||||
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
|
||||
number = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Asset number'))
|
||||
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
|
||||
@@ -236,6 +235,9 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
||||
def __str__(self):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
|
||||
def get_target_ip(self):
|
||||
return self.ip
|
||||
|
||||
def set_admin_user_relation(self):
|
||||
from .authbook import AuthBook
|
||||
if not self.admin_user:
|
||||
@@ -281,16 +283,44 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
||||
def is_support_ansible(self):
|
||||
return self.has_protocol('ssh') and self.platform_base not in ("Other",)
|
||||
|
||||
def get_auth_info(self):
|
||||
def get_auth_info(self, with_become=False):
|
||||
if not self.admin_user:
|
||||
return {}
|
||||
|
||||
self.admin_user.load_asset_special_auth(self)
|
||||
if self.is_unixlike() and self.admin_user.su_enabled and self.admin_user.su_from:
|
||||
auth_user = self.admin_user.su_from
|
||||
become_user = self.admin_user
|
||||
else:
|
||||
auth_user = self.admin_user
|
||||
become_user = None
|
||||
|
||||
auth_user.load_asset_special_auth(self)
|
||||
info = {
|
||||
'username': self.admin_user.username,
|
||||
'password': self.admin_user.password,
|
||||
'private_key': self.admin_user.private_key_file,
|
||||
'username': auth_user.username,
|
||||
'password': auth_user.password,
|
||||
'private_key': auth_user.private_key_file
|
||||
}
|
||||
|
||||
if not with_become or self.is_windows():
|
||||
return info
|
||||
|
||||
if become_user:
|
||||
become_user.load_asset_special_auth(self)
|
||||
become_method = 'su'
|
||||
become_username = become_user.username
|
||||
become_pass = become_user.password
|
||||
else:
|
||||
become_method = 'sudo'
|
||||
become_username = 'root'
|
||||
become_pass = auth_user.password
|
||||
become_info = {
|
||||
'become': {
|
||||
'method': become_method,
|
||||
'username': become_username,
|
||||
'pass': become_pass
|
||||
}
|
||||
}
|
||||
info.update(become_info)
|
||||
return info
|
||||
|
||||
def nodes_display(self):
|
||||
@@ -355,3 +385,11 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
ordering = ["hostname", ]
|
||||
permissions = [
|
||||
('refresh_assethardwareinfo', _('Can refresh asset hardware info')),
|
||||
('test_assetconnectivity', _('Can test asset connectivity')),
|
||||
('push_assetsystemuser', _('Can push system user to asset')),
|
||||
('match_asset', _('Can match asset')),
|
||||
('add_assettonode', _('Add asset to node')),
|
||||
('move_assettonode', _('Move asset to node')),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
@@ -25,6 +26,11 @@ class AuthBook(BaseUser, AbsConnectivity):
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
unique_together = [('username', 'asset', 'systemuser')]
|
||||
permissions = [
|
||||
('test_authbook', _('Can test asset account connectivity')),
|
||||
('view_assetaccountsecret', _('Can view asset account secret')),
|
||||
('change_assetaccountsecret', _('Can change asset account secret'))
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -116,6 +122,15 @@ class AuthBook(BaseUser, AbsConnectivity):
|
||||
self.asset.save()
|
||||
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls):
|
||||
queryset = cls.objects.all() \
|
||||
.annotate(ip=F('asset__ip')) \
|
||||
.annotate(hostname=F('asset__hostname')) \
|
||||
.annotate(platform=F('asset__platform__name')) \
|
||||
.annotate(protocols=F('asset__protocols'))
|
||||
return queryset
|
||||
|
||||
def __str__(self):
|
||||
return self.smart_name
|
||||
|
||||
|
||||
145
apps/assets/models/backup.py
Normal file
145
apps/assets/models/backup.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
|
||||
from celery import current_task
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from ops.mixin import PeriodTaskModelMixin
|
||||
from common.utils import get_logger
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
from common.db.models import BitOperationChoice
|
||||
from common.mixins.models import CommonModelMixin
|
||||
|
||||
__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution', 'Type']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class Type(BitOperationChoice):
|
||||
NONE = 0
|
||||
ALL = 0xff
|
||||
|
||||
Asset = 0b1
|
||||
App = 0b1 << 1
|
||||
|
||||
DB_CHOICES = (
|
||||
(ALL, _('All')),
|
||||
(Asset, _('Asset')),
|
||||
(App, _('Application'))
|
||||
)
|
||||
|
||||
NAME_MAP = {
|
||||
ALL: "all",
|
||||
Asset: "asset",
|
||||
App: "application"
|
||||
}
|
||||
|
||||
NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()}
|
||||
CHOICES = []
|
||||
for i, j in DB_CHOICES:
|
||||
CHOICES.append((NAME_MAP[i], j))
|
||||
|
||||
|
||||
class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
types = models.IntegerField(choices=Type.DB_CHOICES, default=Type.ALL, verbose_name=_('Type'))
|
||||
recipients = models.ManyToManyField(
|
||||
'users.User', related_name='recipient_escape_route_plans', blank=True,
|
||||
verbose_name=_("Recipient")
|
||||
)
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}({self.org_id})'
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _('Account backup plan')
|
||||
|
||||
def get_register_task(self):
|
||||
from ..tasks import execute_account_backup_plan
|
||||
name = "account_backup_plan_period_{}".format(str(self.id)[:8])
|
||||
task = execute_account_backup_plan.name
|
||||
args = (str(self.id), AccountBackupPlanExecution.Trigger.timing)
|
||||
kwargs = {}
|
||||
return name, task, args, kwargs
|
||||
|
||||
def to_attr_json(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'is_periodic': self.is_periodic,
|
||||
'interval': self.interval,
|
||||
'crontab': self.crontab,
|
||||
'org_id': self.org_id,
|
||||
'created_by': self.created_by,
|
||||
'types': Type.value_to_choices(self.types),
|
||||
'recipients': {
|
||||
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
||||
for recipient in self.recipients.all()
|
||||
}
|
||||
}
|
||||
|
||||
def execute(self, trigger):
|
||||
try:
|
||||
hid = current_task.request.id
|
||||
except AttributeError:
|
||||
hid = str(uuid.uuid4())
|
||||
execution = AccountBackupPlanExecution.objects.create(
|
||||
id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger
|
||||
)
|
||||
return execution.start()
|
||||
|
||||
|
||||
class AccountBackupPlanExecution(OrgModelMixin):
|
||||
class Trigger(models.TextChoices):
|
||||
manual = 'manual', _('Manual trigger')
|
||||
timing = 'timing', _('Timing trigger')
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
date_start = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_('Date start')
|
||||
)
|
||||
timedelta = models.FloatField(
|
||||
default=0.0, verbose_name=_('Time'), null=True
|
||||
)
|
||||
plan_snapshot = models.JSONField(
|
||||
encoder=ModelJSONFieldEncoder, default=dict,
|
||||
blank=True, null=True, verbose_name=_('Account backup snapshot')
|
||||
)
|
||||
trigger = models.CharField(
|
||||
max_length=128, default=Trigger.manual, choices=Trigger.choices,
|
||||
verbose_name=_('Trigger mode')
|
||||
)
|
||||
reason = models.CharField(
|
||||
max_length=1024, blank=True, null=True, verbose_name=_('Reason')
|
||||
)
|
||||
is_success = models.BooleanField(default=False, verbose_name=_('Is success'))
|
||||
plan = models.ForeignKey(
|
||||
'AccountBackupPlan', related_name='execution', on_delete=models.CASCADE,
|
||||
verbose_name=_('Account backup plan')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account backup execution')
|
||||
|
||||
@property
|
||||
def types(self):
|
||||
types = self.plan_snapshot.get('types')
|
||||
return types
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
recipients = self.plan_snapshot.get('recipients')
|
||||
if not recipients:
|
||||
return []
|
||||
return recipients.values()
|
||||
|
||||
def start(self):
|
||||
from ..task_handlers import ExecutionManager
|
||||
manager = ExecutionManager(execution=self)
|
||||
return manager.run()
|
||||
@@ -19,7 +19,7 @@ from common.utils import (
|
||||
)
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from common.db import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
|
||||
@@ -4,15 +4,19 @@ import uuid
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from users.models import User, UserGroup
|
||||
from applications.models import Application
|
||||
from ..models import SystemUser, Asset
|
||||
|
||||
from common.utils import lazyproperty, get_logger, get_object_or_none
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'CommandFilter', 'CommandFilterRule'
|
||||
]
|
||||
@@ -72,11 +76,16 @@ class CommandFilterRule(OrgModelMixin):
|
||||
confirm = 2, _('Reconfirm')
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
|
||||
filter = models.ForeignKey(
|
||||
'CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules'
|
||||
)
|
||||
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
|
||||
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)])
|
||||
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)]
|
||||
)
|
||||
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
||||
ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case'))
|
||||
action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
|
||||
# 动作: 附加字段
|
||||
# - confirm: 命令复核人
|
||||
@@ -121,13 +130,16 @@ class CommandFilterRule(OrgModelMixin):
|
||||
regex.append(r'\b{0}\b'.format(cmd))
|
||||
else:
|
||||
regex.append(r'\b{0}'.format(cmd))
|
||||
s = r'(?i){}'.format('|'.join(regex))
|
||||
s = r'{}'.format('|'.join(regex))
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def compile_regex(regex):
|
||||
def compile_regex(regex, ignore_case):
|
||||
try:
|
||||
pattern = re.compile(regex)
|
||||
if ignore_case:
|
||||
pattern = re.compile(regex, re.IGNORECASE)
|
||||
else:
|
||||
pattern = re.compile(regex)
|
||||
except Exception as e:
|
||||
error = _('The generated regular expression is incorrect: {}').format(str(e))
|
||||
logger.error(error)
|
||||
@@ -135,7 +147,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
return True, '', pattern
|
||||
|
||||
def match(self, data):
|
||||
succeed, error, pattern = self.compile_regex(regex=self.pattern)
|
||||
succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case)
|
||||
if not succeed:
|
||||
return self.ACTION_UNKNOWN, ''
|
||||
|
||||
@@ -169,6 +181,46 @@ class CommandFilterRule(OrgModelMixin):
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(applicant=session.user_obj)
|
||||
applicant = session.user_obj
|
||||
assignees = self.reviewers.all()
|
||||
ticket.create_process_map_and_node(assignees, applicant)
|
||||
ticket.open(applicant)
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
|
||||
asset_id=None, application_id=None, org_id=None):
|
||||
user_groups = []
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if user:
|
||||
user_groups.extend(list(user.groups.all()))
|
||||
user_group = get_object_or_none(UserGroup, pk=user_group_id)
|
||||
if user_group:
|
||||
org_id = user_group.org_id
|
||||
user_groups.append(user_group)
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
application = get_object_or_none(Application, pk=application_id)
|
||||
q = Q()
|
||||
if user:
|
||||
q |= Q(users=user)
|
||||
if user_groups:
|
||||
q |= Q(user_groups__in=set(user_groups))
|
||||
if system_user:
|
||||
org_id = system_user.org_id
|
||||
q |= Q(system_users=system_user)
|
||||
if asset:
|
||||
org_id = asset.org_id
|
||||
q |= Q(assets=asset)
|
||||
if application:
|
||||
org_id = application.org_id
|
||||
q |= Q(applications=application)
|
||||
if q:
|
||||
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
|
||||
if org_id:
|
||||
cmd_filters = cmd_filters.filter(org_id=org_id)
|
||||
rule_ids = cmd_filters.values_list('rules', flat=True)
|
||||
rules = cls.objects.filter(id__in=rule_ids)
|
||||
else:
|
||||
rules = cls.objects.none()
|
||||
return rules
|
||||
|
||||
@@ -7,7 +7,6 @@ import random
|
||||
from django.core.cache import cache
|
||||
import paramiko
|
||||
from django.db import models
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
@@ -55,7 +54,7 @@ class Gateway(BaseUser):
|
||||
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
|
||||
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
|
||||
|
||||
class Protocol(TextChoices):
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
|
||||
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
|
||||
@@ -71,6 +70,9 @@ class Gateway(BaseUser):
|
||||
class Meta:
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Gateway")
|
||||
permissions = [
|
||||
('test_gateway', _('Test gateway'))
|
||||
]
|
||||
|
||||
def set_unconnective(self):
|
||||
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||
|
||||
@@ -37,3 +37,4 @@ class Label(OrgModelMixin):
|
||||
class Meta:
|
||||
db_table = "assets_label"
|
||||
unique_together = [('name', 'value', 'org_id')]
|
||||
verbose_name = _('Label')
|
||||
|
||||
@@ -558,6 +558,9 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Node")
|
||||
ordering = ['parent_key', 'value']
|
||||
permissions = [
|
||||
('match_node', _('Can match node')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils import signer, get_object_or_none
|
||||
from common.db.models import TextChoices
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
from .authbook import AuthBook
|
||||
@@ -23,7 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
class ProtocolMixin:
|
||||
protocol: str
|
||||
|
||||
class Protocol(TextChoices):
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
@@ -33,6 +32,8 @@ class ProtocolMixin:
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
postgresql = 'postgresql', 'PostgreSQL'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
k8s = 'k8s', 'K8S'
|
||||
|
||||
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
|
||||
@@ -44,7 +45,9 @@ class ProtocolMixin:
|
||||
Protocol.rdp
|
||||
]
|
||||
APPLICATION_CATEGORY_DB_PROTOCOLS = [
|
||||
Protocol.mysql, Protocol.oracle, Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver
|
||||
Protocol.mysql, Protocol.mariadb, Protocol.oracle,
|
||||
Protocol.postgresql, Protocol.sqlserver,
|
||||
Protocol.redis, Protocol.mongodb
|
||||
]
|
||||
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
|
||||
Protocol.k8s
|
||||
@@ -130,11 +133,23 @@ class AuthMixin:
|
||||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
|
||||
# 清除认证信息
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
# 加载临时认证信息
|
||||
|
||||
# 先加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self._load_tmp_auth_if_has(app_id, user_id)
|
||||
return
|
||||
|
||||
# Remote app
|
||||
from applications.models import Application
|
||||
app = get_object_or_none(Application, pk=app_id)
|
||||
if app and app.category_remote_app:
|
||||
# Remote app
|
||||
self._load_remoteapp_more_auth(app, username, user_id)
|
||||
return
|
||||
|
||||
# Other app
|
||||
# 更新用户名
|
||||
from users.models import User
|
||||
user = get_object_or_none(User, pk=user_id) if user_id else None
|
||||
@@ -145,17 +160,44 @@ class AuthMixin:
|
||||
_username = username
|
||||
self.username = _username
|
||||
|
||||
def _load_remoteapp_more_auth(self, app, username, user_id):
|
||||
asset = app.get_remote_app_asset(raise_exception=False)
|
||||
if asset:
|
||||
self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id)
|
||||
|
||||
def load_asset_special_auth(self, asset, username=''):
|
||||
"""
|
||||
AuthBook 的数据状态
|
||||
| asset | systemuser | username |
|
||||
1 | * | * | x |
|
||||
2 | * | x | * |
|
||||
|
||||
当前 AuthBook 只有以上两种状态,systemuser 与 username 不会并存。
|
||||
正常的资产与系统用户关联产生的是第1种状态,改密则产生第2种状态。改密之后
|
||||
只有 username 而没有 systemuser 。
|
||||
|
||||
Freq: 关联同一资产的多个系统用户指定同一用户名时,修改用户密码会影响所有系统用户
|
||||
|
||||
这里有一个不对称的行为,同名系统用户密码覆盖
|
||||
当有相同 username 的多个系统用户时,有改密动作之后,所有的同名系统用户都使用最后
|
||||
一次改动,但如果没有发生过改密,同名系统用户使用的密码还是各自的。
|
||||
|
||||
"""
|
||||
authbooks = list(AuthBook.objects.filter(asset=asset, systemuser=self))
|
||||
if len(authbooks) == 0:
|
||||
if username == '':
|
||||
username = self.username
|
||||
|
||||
authbook = AuthBook.objects.filter(
|
||||
asset=asset, username=username, systemuser__isnull=True
|
||||
).order_by('-date_created').first()
|
||||
|
||||
if not authbook:
|
||||
authbook = AuthBook.objects.filter(
|
||||
asset=asset, systemuser=self
|
||||
).order_by('-date_created').first()
|
||||
|
||||
if not authbook:
|
||||
return None
|
||||
elif len(authbooks) == 1:
|
||||
authbook = authbooks[0]
|
||||
else:
|
||||
authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True)
|
||||
authbook = authbooks[0]
|
||||
|
||||
authbook.load_auth()
|
||||
self.password = authbook.password
|
||||
self.private_key = authbook.private_key
|
||||
@@ -192,7 +234,7 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
||||
(LOGIN_MANUAL, _('Manually input'))
|
||||
)
|
||||
|
||||
class Type(TextChoices):
|
||||
class Type(models.TextChoices):
|
||||
common = 'common', _('Common user')
|
||||
admin = 'admin', _('Admin user')
|
||||
|
||||
@@ -298,9 +340,12 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("System user")
|
||||
permissions = [
|
||||
('match_systemuser', _('Can match system user')),
|
||||
]
|
||||
|
||||
|
||||
# Todo: 准备废弃
|
||||
# Deprecated: 准备废弃
|
||||
class AdminUser(BaseUser):
|
||||
"""
|
||||
A privileged user that ansible can use it to push system user and so on
|
||||
|
||||
25
apps/assets/notifications.py
Normal file
25
apps/assets/notifications.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from users.models import User
|
||||
from common.tasks import send_mail_attachment_async
|
||||
|
||||
|
||||
class AccountBackupExecutionTaskMsg(object):
|
||||
subject = _('Notification of account backup route task results')
|
||||
|
||||
def __init__(self, name: str, user: User):
|
||||
self.name = name
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
name = self.name
|
||||
if self.user.secret_key:
|
||||
return _('{} - The account backup passage task has been completed. See the attachment for details').format(name)
|
||||
return _("{} - The account backup passage task has been completed: the encryption password has not been set - "
|
||||
"please go to personal information -> file encryption password to set the encryption password").format(name)
|
||||
|
||||
def publish(self, attachment_list=None):
|
||||
send_mail_attachment_async.delay(
|
||||
self.subject, self.message, [self.user.email], attachment_list
|
||||
)
|
||||
@@ -11,3 +11,4 @@ from .cmd_filter import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .account import *
|
||||
from .backup import *
|
||||
|
||||
@@ -5,33 +5,64 @@ from assets.models import AuthBook
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializerMixin
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
|
||||
|
||||
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
ip = serializers.ReadOnlyField(label=_("IP"))
|
||||
hostname = serializers.ReadOnlyField(label=_("Hostname"))
|
||||
platform = serializers.ReadOnlyField(label=_("Platform"))
|
||||
protocols = serializers.SerializerMethodField(label=_("Protocols"))
|
||||
date_created = serializers.DateTimeField(
|
||||
label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True
|
||||
)
|
||||
date_updated = serializers.DateTimeField(
|
||||
label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields_mini = ['id', 'username', 'ip', 'hostname', 'version']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_mini = ['id', 'username', 'ip', 'hostname', 'platform', 'protocols', 'version']
|
||||
fields_write_only = ['password', 'private_key', "public_key", 'passphrase']
|
||||
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
|
||||
fields_small = fields_mini + fields_write_only + fields_other
|
||||
fields_fk = ['asset', 'systemuser', 'systemuser_display']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {
|
||||
'write_only': True,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
'systemuser_display': {'label': _('System user display')}
|
||||
}
|
||||
ref_name = 'AssetAccountSerializer'
|
||||
|
||||
def _validate_gen_key(self, attrs):
|
||||
private_key = attrs.get('private_key')
|
||||
if not private_key:
|
||||
return attrs
|
||||
|
||||
password = attrs.get('passphrase')
|
||||
username = attrs.get('username')
|
||||
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
|
||||
attrs['public_key'] = public_key
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
return attrs
|
||||
|
||||
def get_protocols(self, v):
|
||||
""" protocols 是 queryset 中返回的,Post 创建成功后返回序列化时没有这个字段 """
|
||||
if hasattr(v, 'protocols'):
|
||||
protocols = v.protocols
|
||||
elif hasattr(v, 'asset') and v.asset:
|
||||
protocols = v.asset.protocols
|
||||
else:
|
||||
protocols = ''
|
||||
protocols = protocols.replace(' ', ', ')
|
||||
return protocols
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
@@ -43,8 +74,12 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AccountSecretSerializer(AccountSerializer):
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
|
||||
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
|
||||
@@ -15,6 +15,7 @@ class AdminUserSerializer(SuS):
|
||||
SuS.Meta.fields_m2m + \
|
||||
[
|
||||
'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint',
|
||||
'su_enabled', 'su_from',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ from django.core.validators import RegexValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from users.models import User, UserGroup
|
||||
from perms.models import AssetPermission
|
||||
from ..models import Asset, Node, Platform, SystemUser
|
||||
|
||||
__all__ = [
|
||||
|
||||
52
apps/assets/serializers/backup.py
Normal file
52
apps/assets/serializers/backup.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ops.mixin import PeriodTaskSerializerMixin
|
||||
from common.utils import get_logger
|
||||
|
||||
from .base import TypesField
|
||||
|
||||
from ..models import AccountBackupPlan, AccountBackupPlanExecution
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['AccountBackupPlanSerializer', 'AccountBackupPlanExecutionSerializer']
|
||||
|
||||
|
||||
class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
types = TypesField(required=False, allow_null=True, label=_("Actions"))
|
||||
|
||||
class Meta:
|
||||
model = AccountBackupPlan
|
||||
fields = [
|
||||
'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created',
|
||||
'date_updated', 'created_by', 'periodic_display', 'comment',
|
||||
'recipients', 'types'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
'periodic_display': {'label': _('Periodic perform')},
|
||||
'recipients': {'label': _('Recipient'), 'help_text': _(
|
||||
'Currently only mail sending is supported'
|
||||
)}
|
||||
}
|
||||
|
||||
|
||||
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
||||
trigger_display = serializers.ReadOnlyField(
|
||||
source='get_trigger_display', label=_('Trigger mode')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AccountBackupPlanExecution
|
||||
fields = [
|
||||
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
||||
'is_success', 'plan', 'org_id', 'recipients', 'trigger_display'
|
||||
]
|
||||
read_only_fields = (
|
||||
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
||||
'is_success', 'org_id', 'recipients'
|
||||
)
|
||||
@@ -1,15 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from io import StringIO
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, validate_ssh_private_key
|
||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
|
||||
from common.drf.fields import EncryptedField
|
||||
from assets.models import Type
|
||||
from .utils import validate_password_for_ansible
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key'))
|
||||
|
||||
def gen_keys(self, private_key=None, password=None):
|
||||
if private_key is None:
|
||||
@@ -28,17 +32,35 @@ class AuthSerializer(serializers.ModelSerializer):
|
||||
return self.instance
|
||||
|
||||
|
||||
class AuthSerializerMixin:
|
||||
class AuthSerializerMixin(serializers.ModelSerializer):
|
||||
password = EncryptedField(
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
||||
validators=[validate_password_for_ansible]
|
||||
)
|
||||
private_key = EncryptedField(
|
||||
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||
write_only=True, label=_('Key password')
|
||||
)
|
||||
|
||||
def validate_password(self, password):
|
||||
return password
|
||||
|
||||
def validate_private_key(self, private_key):
|
||||
if not private_key:
|
||||
return
|
||||
password = self.initial_data.get("password")
|
||||
valid = validate_ssh_private_key(private_key, password)
|
||||
passphrase = self.initial_data.get('passphrase')
|
||||
passphrase = passphrase if passphrase else None
|
||||
valid = validate_ssh_private_key(private_key, password=passphrase)
|
||||
if not valid:
|
||||
raise serializers.ValidationError(_("private key invalid"))
|
||||
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
||||
|
||||
private_key = ssh_private_key_gen(private_key, password=passphrase)
|
||||
string_io = StringIO()
|
||||
private_key.write_private_key(string_io)
|
||||
private_key = string_io.getvalue()
|
||||
return private_key
|
||||
|
||||
def validate_public_key(self, public_key):
|
||||
@@ -50,6 +72,7 @@ class AuthSerializerMixin:
|
||||
value = validated_data.get(field)
|
||||
if not value:
|
||||
validated_data.pop(field, None)
|
||||
validated_data.pop('passphrase', None)
|
||||
|
||||
def create(self, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
@@ -58,3 +81,24 @@ class AuthSerializerMixin:
|
||||
def update(self, instance, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class TypesField(serializers.MultipleChoiceField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['choices'] = Type.CHOICES
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return Type.value_to_choices(value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data is None:
|
||||
return data
|
||||
return Type.choices_to_value(data)
|
||||
|
||||
|
||||
class ActionsDisplayField(TypesField):
|
||||
def to_representation(self, value):
|
||||
values = super().to_representation(value)
|
||||
choices = dict(Type.CHOICES)
|
||||
return [choices.get(i) for i in values]
|
||||
|
||||
@@ -12,13 +12,11 @@ from terminal.models import Session
|
||||
|
||||
|
||||
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'org_id', 'org_name',
|
||||
'is_active',
|
||||
'org_id', 'org_name', 'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
@@ -26,25 +24,32 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
extra_kwargs = {
|
||||
'rules': {'read_only': True}
|
||||
'rules': {'read_only': True},
|
||||
'date_created': {'label': _("Date created")},
|
||||
'date_updated': {'label': _("Date updated")},
|
||||
}
|
||||
|
||||
|
||||
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display')
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display"))
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'content', 'pattern', 'priority',
|
||||
'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
'type', 'type_display', 'content', 'ignore_case', 'pattern',
|
||||
'priority', 'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'date_created': {'label': _("Date created")},
|
||||
'date_updated': {'label': _("Date updated")},
|
||||
'action_display': {'label': _("Action display")},
|
||||
'pattern': {'label': _("Pattern")}
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -66,7 +71,8 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
regex = CommandFilterRule.construct_command_regex(content)
|
||||
else:
|
||||
regex = content
|
||||
succeed, error, pattern = CommandFilterRule.compile_regex(regex)
|
||||
ignore_case = self.initial_data.get('ignore_case')
|
||||
succeed, error, pattern = CommandFilterRule.compile_regex(regex, ignore_case)
|
||||
if not succeed:
|
||||
raise serializers.ValidationError(error)
|
||||
return content
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.validators import alphanumeric
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from ..models import Domain, Gateway
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
@@ -43,13 +44,13 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
is_connective = serializers.BooleanField(required=False)
|
||||
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields_mini = ['id', 'name']
|
||||
fields_write_only = [
|
||||
'password', 'private_key', 'public_key',
|
||||
'password', 'private_key', 'public_key', 'passphrase'
|
||||
]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'username', 'ip', 'port', 'protocol',
|
||||
@@ -67,7 +68,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
|
||||
class Meta(GatewaySerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user