mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-16 17:12:53 +00:00
Compare commits
485 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c13df19e | ||
|
|
180cf354ad | ||
|
|
89a5c970e4 | ||
|
|
1d25cad449 | ||
|
|
c3b0798311 | ||
|
|
ff851b4672 | ||
|
|
2bcdcce2d3 | ||
|
|
f5ac941eb3 | ||
|
|
efcbfe63f9 | ||
|
|
41a2e00406 | ||
|
|
738b9efe11 | ||
|
|
119c7a8634 | ||
|
|
77e43c1c5c | ||
|
|
4d0231a9ad | ||
|
|
4562f1fbe8 | ||
|
|
4be70ff3da | ||
|
|
1a742d65f6 | ||
|
|
ba83b64d87 | ||
|
|
f46c9f56e8 | ||
|
|
626ec8f25d | ||
|
|
526c7de598 | ||
|
|
03273b2ec4 | ||
|
|
737cae8d03 | ||
|
|
cf6ce0fa2e | ||
|
|
7dd6ee5f1a | ||
|
|
91432f0e8f | ||
|
|
6c36b5be92 | ||
|
|
7b89055fbf | ||
|
|
c0f3769f9f | ||
|
|
b20abb494f | ||
|
|
a084bc9962 | ||
|
|
cbb615e2ce | ||
|
|
769d5fbd96 | ||
|
|
bbd36fea03 | ||
|
|
9317d9e35e | ||
|
|
f697033252 | ||
|
|
eb8d80d417 | ||
|
|
d5ac8b16f1 | ||
|
|
ed54cc8507 | ||
|
|
40248077cd | ||
|
|
45e1723aa9 | ||
|
|
af9f7060be | ||
|
|
8f10b84e94 | ||
|
|
d02cbcc3a3 | ||
|
|
689fd12141 | ||
|
|
3c9c494979 | ||
|
|
16ceb79427 | ||
|
|
cd5e53e3dc | ||
|
|
df1aa73723 | ||
|
|
ceee2e1633 | ||
|
|
91867fa01d | ||
|
|
dfde9258c7 | ||
|
|
fc595bc4e4 | ||
|
|
48aa48e7a3 | ||
|
|
479378aa46 | ||
|
|
362c2a9509 | ||
|
|
a423d241a5 | ||
|
|
9e6221443e | ||
|
|
12744a08af | ||
|
|
5e29c7e7bf | ||
|
|
02f38fe37a | ||
|
|
663ccbca6f | ||
|
|
c4528612d5 | ||
|
|
7707101379 | ||
|
|
873e6d1ab9 | ||
|
|
7ba261c4f0 | ||
|
|
1f8428ac1c | ||
|
|
8e0c04c84c | ||
|
|
a6e49b730b | ||
|
|
c11ba16e4e | ||
|
|
efe57b3ebe | ||
|
|
4899f6bb69 | ||
|
|
ef0c2f41ac | ||
|
|
98b4f51cbb | ||
|
|
da52180976 | ||
|
|
bd642a0281 | ||
|
|
dc88e4f420 | ||
|
|
7a3a0b2d8e | ||
|
|
eac1b287e4 | ||
|
|
d2f7396689 | ||
|
|
db4f05afbe | ||
|
|
339fe1b73b | ||
|
|
237c71f921 | ||
|
|
bd7c5f8e65 | ||
|
|
c3ea5300a3 | ||
|
|
e2de744398 | ||
|
|
a890a8d535 | ||
|
|
c39e134834 | ||
|
|
e9e5fbb4c2 | ||
|
|
3203c298e5 | ||
|
|
e416a5d5d7 | ||
|
|
7ea61c0f22 | ||
|
|
b2108ec624 | ||
|
|
433324ec8c | ||
|
|
ac20bfe024 | ||
|
|
a116c7db39 | ||
|
|
71e69782b7 | ||
|
|
7611d4e7ce | ||
|
|
a778a40b21 | ||
|
|
4e254493bc | ||
|
|
07530bc56b | ||
|
|
259daaab38 | ||
|
|
c769c06202 | ||
|
|
e0463420fa | ||
|
|
1944e80418 | ||
|
|
4b72099053 | ||
|
|
dcf113b87c | ||
|
|
ab6d0d2484 | ||
|
|
7bef4b07ff | ||
|
|
f486c843bf | ||
|
|
90038e41f9 | ||
|
|
33ee84633f | ||
|
|
419806aa57 | ||
|
|
8ea3c3288b | ||
|
|
99ce2bc946 | ||
|
|
9bf76ae07a | ||
|
|
a33540710e | ||
|
|
680d31dad2 | ||
|
|
a297355a0d | ||
|
|
e891283925 | ||
|
|
c72ec5ea78 | ||
|
|
b764827003 | ||
|
|
a261b2de3c | ||
|
|
e939776da0 | ||
|
|
0a9726d845 | ||
|
|
c21fcacf70 | ||
|
|
f588a112fb | ||
|
|
ecca64ef42 | ||
|
|
56a657827a | ||
|
|
38803518fc | ||
|
|
c2f1e4f4f6 | ||
|
|
49662b308d | ||
|
|
7636255533 | ||
|
|
8accd296b8 | ||
|
|
e424e3c311 | ||
|
|
e38dd96d6f | ||
|
|
170f1e40d6 | ||
|
|
2aacb07b15 | ||
|
|
6b9f40d5c1 | ||
|
|
27c4e1d895 | ||
|
|
65916a469c | ||
|
|
ff2aace569 | ||
|
|
8cfec07faa | ||
|
|
4dc6bd3660 | ||
|
|
ee874f3ddc | ||
|
|
9691125c7a | ||
|
|
41fa1d65ff | ||
|
|
6d2e7cf7f4 | ||
|
|
4ef05a1cd4 | ||
|
|
207d015497 | ||
|
|
85058f8599 | ||
|
|
55dad53934 | ||
|
|
958290529a | ||
|
|
ba128e99f9 | ||
|
|
89c4a8d5c4 | ||
|
|
6d758bdb59 | ||
|
|
eb8e7c5f8a | ||
|
|
ef4f1ddb74 | ||
|
|
e14e5b523a | ||
|
|
99ae0066ae | ||
|
|
d486dfc7f7 | ||
|
|
93ba4443dd | ||
|
|
d182d14e26 | ||
|
|
8ed823d587 | ||
|
|
44397caad4 | ||
|
|
d17e2cde06 | ||
|
|
681988f450 | ||
|
|
6b333adc05 | ||
|
|
5207b99696 | ||
|
|
b93b64255b | ||
|
|
f9c9c9d525 | ||
|
|
1ad0a20627 | ||
|
|
0ed929a3b2 | ||
|
|
2ffadcb9bc | ||
|
|
3b615719fe | ||
|
|
7776158279 | ||
|
|
47dd73eb4c | ||
|
|
bf30be2084 | ||
|
|
39d651dd9b | ||
|
|
07f4fdd92d | ||
|
|
53c8c2d9ea | ||
|
|
c201914bc8 | ||
|
|
83917cb440 | ||
|
|
b55eb1236f | ||
|
|
38cee8eaa4 | ||
|
|
e339a56042 | ||
|
|
384b639dd3 | ||
|
|
c86b28a305 | ||
|
|
dbfb9db5c5 | ||
|
|
93350faa08 | ||
|
|
107fda0f99 | ||
|
|
58124af1ce | ||
|
|
1a4c5dca33 | ||
|
|
5380dc0c2d | ||
|
|
2c22396093 | ||
|
|
31da139eb3 | ||
|
|
962354c50d | ||
|
|
1907c795c3 | ||
|
|
1239ffd4c8 | ||
|
|
7a37f91964 | ||
|
|
2741d7cbdc | ||
|
|
99adb6ab7a | ||
|
|
665c833479 | ||
|
|
77944cc91b | ||
|
|
b5fc865cc6 | ||
|
|
3b6c2fc0c0 | ||
|
|
114645732a | ||
|
|
1b338a9cd3 | ||
|
|
59f12a3c14 | ||
|
|
3fc52cbb68 | ||
|
|
b0b6d19bc0 | ||
|
|
9deb48b16b | ||
|
|
48510e98a2 | ||
|
|
c135837372 | ||
|
|
92ed189453 | ||
|
|
418ac5a5ba | ||
|
|
539a6161e6 | ||
|
|
806baeb136 | ||
|
|
ae0daddbea | ||
|
|
76903977eb | ||
|
|
c9fffa50a8 | ||
|
|
6478727cd2 | ||
|
|
a20b210514 | ||
|
|
04a34e8456 | ||
|
|
4d2c4a9602 | ||
|
|
2a24fcc1bb | ||
|
|
366693783c | ||
|
|
0a611a4ce9 | ||
|
|
5fedb5440c | ||
|
|
160c99a01a | ||
|
|
089d769eb0 | ||
|
|
9195d4c43d | ||
|
|
f1d984898b | ||
|
|
ecfd9449f2 | ||
|
|
94d40efcad | ||
|
|
d5461fe66f | ||
|
|
00f4ae97ed | ||
|
|
554c1da38b | ||
|
|
f1a68ebd70 | ||
|
|
b443a89cb5 | ||
|
|
5b1ae46153 | ||
|
|
98fd209498 | ||
|
|
7af769f7d3 | ||
|
|
89ec01003c | ||
|
|
148bf3b894 | ||
|
|
38e8e8734d | ||
|
|
d8d487f770 | ||
|
|
e3aaba4798 | ||
|
|
95e92a45d5 | ||
|
|
86a17b9955 | ||
|
|
7ae52eb941 | ||
|
|
b4b9c805ff | ||
|
|
16660575b7 | ||
|
|
e9c2351f83 | ||
|
|
ed49216625 | ||
|
|
2417a0930f | ||
|
|
c9ba3f4f05 | ||
|
|
78d8e410db | ||
|
|
1f25eaf413 | ||
|
|
54e6200ffe | ||
|
|
bad8400e77 | ||
|
|
0fb01bd7fb | ||
|
|
34e7671f65 | ||
|
|
2d99fddaf8 | ||
|
|
5df4efa5a8 | ||
|
|
e2207cf8f1 | ||
|
|
e90e61e8dd | ||
|
|
4c48204e16 | ||
|
|
bddcd8475d | ||
|
|
5f8d84df66 | ||
|
|
cee87ae4d7 | ||
|
|
79a2d4e039 | ||
|
|
4f5e360991 | ||
|
|
8e86173cb8 | ||
|
|
08bc3d14aa | ||
|
|
19b91a6c1f | ||
|
|
c50330e055 | ||
|
|
f5d9dedae1 | ||
|
|
ffb400d70d | ||
|
|
2291cfeaae | ||
|
|
400d37ffca | ||
|
|
14efd9afc1 | ||
|
|
cfca519158 | ||
|
|
23361fdba9 | ||
|
|
1b0d23fbf4 | ||
|
|
de4ef7d1b5 | ||
|
|
046342ceee | ||
|
|
47195e2c44 | ||
|
|
947c9e6216 | ||
|
|
e1af380ad5 | ||
|
|
9e8579d5b4 | ||
|
|
b8397e7db9 | ||
|
|
8ed8d6f01c | ||
|
|
ea607c6177 | ||
|
|
fa52e2bf5e | ||
|
|
02fc9a730b | ||
|
|
aa744c0fec | ||
|
|
02d0c7e4e7 | ||
|
|
0c34a41381 | ||
|
|
8ed3da85f2 | ||
|
|
de5b501ebf | ||
|
|
ea5a54f9c7 | ||
|
|
6338ecc6fe | ||
|
|
be17fe6c31 | ||
|
|
a18c97aec0 | ||
|
|
27c10fcae1 | ||
|
|
539babcc97 | ||
|
|
0436487bdb | ||
|
|
f466904a1c | ||
|
|
1d6bdc9b6b | ||
|
|
d965ac0781 | ||
|
|
6035241efb | ||
|
|
0771b804d1 | ||
|
|
a2c6e5f3fb | ||
|
|
c39041fe7b | ||
|
|
22588c52a9 | ||
|
|
daef154622 | ||
|
|
7b9c4b300d | ||
|
|
819853eae4 | ||
|
|
f686f9f107 | ||
|
|
8a89ee7ac0 | ||
|
|
696295cf0d | ||
|
|
d99a3455cd | ||
|
|
7f5b0618c6 | ||
|
|
0f1d9bc3eb | ||
|
|
8f6b8b5a11 | ||
|
|
4da0fadcc4 | ||
|
|
f504413d7f | ||
|
|
9b5803f2a2 | ||
|
|
d95e7c2e24 | ||
|
|
a1ded0c737 | ||
|
|
bedc83bd3a | ||
|
|
c9f3e4b28d | ||
|
|
05bbd22c44 | ||
|
|
d00ef2b051 | ||
|
|
efc538a569 | ||
|
|
c1de9151b8 | ||
|
|
2898d25bf8 | ||
|
|
68e2de81d8 | ||
|
|
dd5802316d | ||
|
|
6f1ab1e09a | ||
|
|
6096ccc30a | ||
|
|
ddbd142ea3 | ||
|
|
61d8328337 | ||
|
|
4caa704abe | ||
|
|
b75d69de5d | ||
|
|
10fa122e2f | ||
|
|
00ff1644cb | ||
|
|
2b51a7590e | ||
|
|
30d07820c7 | ||
|
|
c51ebd62df | ||
|
|
593e28d7fa | ||
|
|
89f1a1653d | ||
|
|
ad311c15ca | ||
|
|
b10623c970 | ||
|
|
7d17c1a450 | ||
|
|
100b1553b6 | ||
|
|
76af71bbbe | ||
|
|
9607ab5164 | ||
|
|
61078ee2ed | ||
|
|
6a720cde0a | ||
|
|
a2a5d5e08b | ||
|
|
9c2cc65ce8 | ||
|
|
ee3cdcd9e4 | ||
|
|
89492410aa | ||
|
|
b324c6cc8a | ||
|
|
6b189e6162 | ||
|
|
a07cab9ae7 | ||
|
|
751bd35349 | ||
|
|
d6aaf23abb | ||
|
|
f096014d03 | ||
|
|
7f03639c34 | ||
|
|
3963881226 | ||
|
|
fb279dbc39 | ||
|
|
785e4cc3e4 | ||
|
|
dd846d4183 | ||
|
|
9169f3546a | ||
|
|
7e2c0d0a2d | ||
|
|
66c60ef5be | ||
|
|
f095998096 | ||
|
|
d06e5d0001 | ||
|
|
c8f420f62d | ||
|
|
02550b38f8 | ||
|
|
50531d3b97 | ||
|
|
db7ad81103 | ||
|
|
d72ec653f4 | ||
|
|
7950718582 | ||
|
|
998321f090 | ||
|
|
1fa258da3e | ||
|
|
8dbe61100b | ||
|
|
d7f9f3b670 | ||
|
|
8b18f46613 | ||
|
|
eb49beaf46 | ||
|
|
3971fce561 | ||
|
|
2f81196874 | ||
|
|
411102ed85 | ||
|
|
125dc2adf5 | ||
|
|
6001175629 | ||
|
|
41e39c9614 | ||
|
|
19de79fadf | ||
|
|
6b7df10d50 | ||
|
|
ce269e315a | ||
|
|
dfc8654d96 | ||
|
|
ea07f9e56a | ||
|
|
bbbd011cc2 | ||
|
|
6962430e6a | ||
|
|
ca1b82330e | ||
|
|
f4bd06b970 | ||
|
|
d0bf5b46f6 | ||
|
|
3c707996e0 | ||
|
|
ac0a673818 | ||
|
|
1ed6c7e01d | ||
|
|
adcabf69ed | ||
|
|
0b92e43e20 | ||
|
|
9c1a6b8565 | ||
|
|
fc8d226005 | ||
|
|
f3955a47f6 | ||
|
|
0020fe7be0 | ||
|
|
cea56a2f7e | ||
|
|
e3cf6cc476 | ||
|
|
57fccc9baf | ||
|
|
fbcb0da349 | ||
|
|
877a053717 | ||
|
|
d293a03649 | ||
|
|
08e0c5fdf5 | ||
|
|
ac906a5d52 | ||
|
|
9ad8e53743 | ||
|
|
bf29158be9 | ||
|
|
a67ee976b4 | ||
|
|
dfa12239d6 | ||
|
|
4737e2cf4a | ||
|
|
d3d8fcbbb3 | ||
|
|
a64aa89b3f | ||
|
|
a22f36a06a | ||
|
|
17fa139bc9 | ||
|
|
77bcb05d80 | ||
|
|
4e9012cc07 | ||
|
|
b3dce27309 | ||
|
|
bccf3a0340 | ||
|
|
358b3a1891 | ||
|
|
5a2f6bdfc9 | ||
|
|
768eb033eb | ||
|
|
d7d554daf5 | ||
|
|
780b1104de | ||
|
|
eeba0a4bfc | ||
|
|
b2ee8c8216 | ||
|
|
26edd2f040 | ||
|
|
270ed5e2f8 | ||
|
|
b2bff22387 | ||
|
|
1ca71f78ed | ||
|
|
fa24a8e2f3 | ||
|
|
b9c1a89f51 | ||
|
|
a2bbf11f9d | ||
|
|
1d084311c5 | ||
|
|
cb0fd937c8 | ||
|
|
13fc2aa73c | ||
|
|
5d9979ec03 | ||
|
|
e4f21b8a5f | ||
|
|
9403b76333 | ||
|
|
666df6ffef | ||
|
|
9cc3942b3d | ||
|
|
42852c368c | ||
|
|
4d4644dddd | ||
|
|
471411a1aa | ||
|
|
db12bc07e8 | ||
|
|
618ee0b2f9 | ||
|
|
39ba52e4de | ||
|
|
a8ef405939 | ||
|
|
09f7ddd28a | ||
|
|
da4337168f | ||
|
|
f13966e061 | ||
|
|
f4b5a302a1 | ||
|
|
dd955530f1 | ||
|
|
50b64f6cf5 | ||
|
|
a5b21f94c2 | ||
|
|
9e3e183f95 | ||
|
|
9ec3147b5f | ||
|
|
79fa134621 | ||
|
|
ef4132d2c5 | ||
|
|
b31a08ed8d | ||
|
|
cdd47f4bc6 | ||
|
|
269a5e9d52 | ||
|
|
dd0d1d3592 | ||
|
|
c06368d812 | ||
|
|
96ef56da67 |
@@ -1,5 +1,4 @@
|
|||||||
.git
|
.git
|
||||||
logs/*
|
|
||||||
data/*
|
data/*
|
||||||
.github
|
.github
|
||||||
tmp/*
|
tmp/*
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/----.md
vendored
3
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -6,8 +6,7 @@ labels: 类型:需求
|
|||||||
assignees:
|
assignees:
|
||||||
- ibuler
|
- ibuler
|
||||||
- baijiangjie
|
- baijiangjie
|
||||||
|
- wojiushixiaobai
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**请描述您的需求或者改进建议.**
|
**请描述您的需求或者改进建议.**
|
||||||
|
|||||||
31
.github/workflows/issue-comment.yml
vendored
31
.github/workflows/issue-comment.yml
vendored
@@ -21,17 +21,44 @@ jobs:
|
|||||||
actions: 'remove-labels'
|
actions: 'remove-labels'
|
||||||
labels: '状态:待反馈'
|
labels: '状态:待反馈'
|
||||||
|
|
||||||
add-label-if-not-author:
|
add-label-if-is-member:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: (github.event.issue.user.id != github.event.comment.user.id) && !github.event.issue.pull_request && (github.event.issue.state == 'open')
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Get Organization name
|
||||||
|
id: org_name
|
||||||
|
run: echo "data=$(echo '${{ github.repository }}' | cut -d '/' -f 1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get Organization public members
|
||||||
|
uses: octokit/request-action@v2.x
|
||||||
|
id: members
|
||||||
|
with:
|
||||||
|
route: GET /orgs/${{ steps.org_name.outputs.data }}/public_members
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Process public members data
|
||||||
|
# 将 members 中的数据转化为 login 字段的拼接字符串
|
||||||
|
id: member_names
|
||||||
|
run: echo "data=$(echo '${{ steps.members.outputs.data }}' | jq '[.[].login] | join(",")')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
|
||||||
|
- run: "echo members: '${{ steps.members.outputs.data }}'"
|
||||||
|
- run: "echo member names: '${{ steps.member_names.outputs.data }}'"
|
||||||
|
- run: "echo comment user: '${{ github.event.comment.user.login }}'"
|
||||||
|
- run: "echo contains? : '${{ contains(steps.member_names.outputs.data, github.event.comment.user.login) }}'"
|
||||||
|
|
||||||
- name: Add require replay label
|
- name: Add require replay label
|
||||||
|
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||||
uses: actions-cool/issues-helper@v2
|
uses: actions-cool/issues-helper@v2
|
||||||
with:
|
with:
|
||||||
actions: 'add-labels'
|
actions: 'add-labels'
|
||||||
labels: '状态:待反馈'
|
labels: '状态:待反馈'
|
||||||
|
|
||||||
- name: Remove require handle label
|
- name: Remove require handle label
|
||||||
|
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||||
uses: actions-cool/issues-helper@v2
|
uses: actions-cool/issues-helper@v2
|
||||||
with:
|
with:
|
||||||
actions: 'remove-labels'
|
actions: 'remove-labels'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,7 +35,6 @@ celerybeat-schedule.db
|
|||||||
docs/_build/
|
docs/_build/
|
||||||
xpack
|
xpack
|
||||||
xpack.bak
|
xpack.bak
|
||||||
logs/*
|
|
||||||
### Vagrant ###
|
### Vagrant ###
|
||||||
.vagrant/
|
.vagrant/
|
||||||
release/*
|
release/*
|
||||||
|
|||||||
49
Dockerfile
49
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9-slim-buster as stage-build
|
FROM python:3.11-slim-bullseye as stage-build
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
@@ -8,9 +8,8 @@ WORKDIR /opt/jumpserver
|
|||||||
ADD . .
|
ADD . .
|
||||||
RUN cd utils && bash -ixeu build.sh
|
RUN cd utils && bash -ixeu build.sh
|
||||||
|
|
||||||
FROM python:3.9-slim-buster
|
FROM python:3.11-slim-bullseye
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
|
||||||
|
|
||||||
ARG BUILD_DEPENDENCIES=" \
|
ARG BUILD_DEPENDENCIES=" \
|
||||||
g++ \
|
g++ \
|
||||||
@@ -22,8 +21,10 @@ ARG DEPENDENCIES=" \
|
|||||||
libpq-dev \
|
libpq-dev \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
|
libkrb5-dev \
|
||||||
libldap2-dev \
|
libldap2-dev \
|
||||||
libsasl2-dev \
|
libsasl2-dev \
|
||||||
|
libssl-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxmlsec1-dev \
|
libxmlsec1-dev \
|
||||||
libxmlsec1-openssl \
|
libxmlsec1-openssl \
|
||||||
@@ -36,13 +37,11 @@ ARG TOOLS=" \
|
|||||||
default-libmysqlclient-dev \
|
default-libmysqlclient-dev \
|
||||||
default-mysql-client \
|
default-mysql-client \
|
||||||
locales \
|
locales \
|
||||||
|
nmap \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
procps \
|
|
||||||
sshpass \
|
sshpass \
|
||||||
telnet \
|
telnet \
|
||||||
unzip \
|
|
||||||
vim \
|
vim \
|
||||||
git \
|
|
||||||
wget"
|
wget"
|
||||||
|
|
||||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||||
@@ -64,37 +63,17 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
|||||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ARG DOWNLOAD_URL=https://download.jumpserver.org
|
|
||||||
|
|
||||||
RUN mkdir -p /opt/oracle/ \
|
|
||||||
&& cd /opt/oracle/ \
|
|
||||||
&& wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
|
|
||||||
&& unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
|
|
||||||
&& sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \
|
|
||||||
&& ldconfig \
|
|
||||||
&& rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip
|
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
|
||||||
COPY ./requirements ./requirements
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
set -ex \
|
|
||||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
|
||||||
&& pip install --upgrade pip \
|
|
||||||
&& pip install --upgrade setuptools wheel \
|
|
||||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
|
||||||
&& pip install -r requirements/requirements.txt
|
|
||||||
|
|
||||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||||
RUN echo > /opt/jumpserver/config.yml \
|
|
||||||
&& rm -rf /tmp/build
|
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
WORKDIR /opt/jumpserver
|
||||||
|
|
||||||
|
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
set -ex \
|
||||||
|
&& echo > /opt/jumpserver/config.yml \
|
||||||
|
&& pip install poetry -i ${PIP_MIRROR} \
|
||||||
|
&& poetry config virtualenvs.create false \
|
||||||
|
&& poetry install --only=main
|
||||||
|
|
||||||
VOLUME /opt/jumpserver/data
|
VOLUME /opt/jumpserver/data
|
||||||
VOLUME /opt/jumpserver/logs
|
VOLUME /opt/jumpserver/logs
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
ARG VERSION
|
ARG VERSION
|
||||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||||
FROM jumpserver/core:${VERSION}
|
FROM jumpserver/core:${VERSION}
|
||||||
|
|
||||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
set -ex \
|
set -ex \
|
||||||
&& pip install -r requirements/requirements_xpack.txt
|
&& poetry install --only=xpack
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
FROM python:3.9-slim-buster as stage-build
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
ARG VERSION
|
|
||||||
ENV VERSION=$VERSION
|
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
|
||||||
ADD . .
|
|
||||||
RUN cd utils && bash -ixeu build.sh
|
|
||||||
|
|
||||||
FROM python:3.9-slim-buster
|
|
||||||
ARG TARGETARCH
|
|
||||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
|
||||||
|
|
||||||
ARG BUILD_DEPENDENCIES=" \
|
|
||||||
g++ \
|
|
||||||
make \
|
|
||||||
pkg-config"
|
|
||||||
|
|
||||||
ARG DEPENDENCIES=" \
|
|
||||||
freetds-dev \
|
|
||||||
libpq-dev \
|
|
||||||
libffi-dev \
|
|
||||||
libjpeg-dev \
|
|
||||||
libldap2-dev \
|
|
||||||
libsasl2-dev \
|
|
||||||
libssl-dev \
|
|
||||||
libxml2-dev \
|
|
||||||
libxmlsec1-dev \
|
|
||||||
libxmlsec1-openssl \
|
|
||||||
freerdp2-dev \
|
|
||||||
libaio-dev"
|
|
||||||
|
|
||||||
ARG TOOLS=" \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
default-libmysqlclient-dev \
|
|
||||||
default-mysql-client \
|
|
||||||
locales \
|
|
||||||
openssh-client \
|
|
||||||
procps \
|
|
||||||
sshpass \
|
|
||||||
telnet \
|
|
||||||
unzip \
|
|
||||||
vim \
|
|
||||||
git \
|
|
||||||
wget"
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
|
||||||
set -ex \
|
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
|
||||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
|
||||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
|
||||||
&& mkdir -p /root/.ssh/ \
|
|
||||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
|
||||||
&& echo "set mouse-=a" > ~/.vimrc \
|
|
||||||
&& echo "no" | dpkg-reconfigure dash \
|
|
||||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
|
||||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
|
||||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
|
||||||
COPY ./requirements ./requirements
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
set -ex \
|
|
||||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
|
||||||
&& pip install --upgrade pip \
|
|
||||||
&& pip install --upgrade setuptools wheel \
|
|
||||||
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \
|
|
||||||
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \
|
|
||||||
&& pip install https://download.jumpserver.org/pypi/simple/PyNaCl/PyNaCl-1.5.0-cp39-cp39-linux_loongarch64.whl \
|
|
||||||
&& pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.2-cp39-cp39-linux_loongarch64.whl \
|
|
||||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
|
||||||
&& pip install -r requirements/requirements.txt
|
|
||||||
|
|
||||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
|
||||||
RUN echo > /opt/jumpserver/config.yml \
|
|
||||||
&& rm -rf /tmp/build
|
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
|
||||||
VOLUME /opt/jumpserver/data
|
|
||||||
VOLUME /opt/jumpserver/logs
|
|
||||||
|
|
||||||
ENV LANG=zh_CN.UTF-8
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
|
||||||
53
README.md
53
README.md
@@ -17,18 +17,17 @@
|
|||||||
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
| :warning: 注意 :warning: |
|
------------------------------
|
||||||
|:-------------------------------------------------------------------------------------------------------------------------:|
|
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||||
| 3.0 架构上和 2.0 变化较大,建议全新安装一套环境来体验。如需升级,请务必升级前进行备份,并[查阅文档](https://kb.fit2cloud.com/?p=06638d69-f109-4333-b5bf-65b17b297ed9) |
|
|
||||||
|
|
||||||
--------------------------
|
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||||
|
|
||||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
|
||||||
|
|
||||||
- **SSH**: Linux / Unix / 网络设备 等;
|
- **SSH**: Linux / Unix / 网络设备 等;
|
||||||
- **Windows**: Web 方式连接 / 原生 RDP 连接;
|
- **Windows**: Web 方式连接 / 原生 RDP 连接;
|
||||||
- **数据库**: MySQL / Oracle / SQLServer / PostgreSQL 等;
|
- **数据库**: MySQL / MariaDB / PostgreSQL / Oracle / SQLServer / ClickHouse 等;
|
||||||
- **Kubernetes**: 支持连接到 K8s 集群中的 Pods;
|
- **NoSQL**: Redis / MongoDB 等;
|
||||||
|
- **GPT**: ChatGPT 等;
|
||||||
|
- **云服务**: Kubernetes / VMware vSphere 等;
|
||||||
- **Web 站点**: 各类系统的 Web 管理后台;
|
- **Web 站点**: 各类系统的 Web 管理后台;
|
||||||
- **应用**: 通过 Remote App 连接各类应用。
|
- **应用**: 通过 Remote App 连接各类应用。
|
||||||
|
|
||||||
@@ -81,29 +80,28 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运
|
|||||||
|
|
||||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
|
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
|
||||||
|
|
||||||
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 及微信交流群当中进行交流沟通。
|
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
|
||||||
|
|
||||||
**微信交流群**
|
|
||||||
|
|
||||||
<img src="https://download.jumpserver.org/images/wecom-group.jpeg" alt="微信群二维码" width="200"/>
|
|
||||||
|
|
||||||
### 参与贡献
|
### 参与贡献
|
||||||
|
|
||||||
欢迎提交 PR 参与贡献。感谢以下贡献者,他们让 JumpServer 变的越来越好。
|
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
|
||||||
|
|
||||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
|
||||||
|
|
||||||
## 组件项目
|
## 组件项目
|
||||||
|
|
||||||
| 项目 | 状态 | 描述 |
|
| 项目 | 状态 | 描述 |
|
||||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
|
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||||
| [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 项目 |
|
| [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 项目 |
|
| [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) |
|
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目 |
|
||||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
| [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 项目 |
|
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer RDP 代理 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 客户端 项目 |
|
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 |
|
||||||
| [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 安装包 项目 |
|
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||||
|
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
|
||||||
|
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
|
||||||
|
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core Api 通信的组件项目 |
|
||||||
|
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||||
|
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||||
|
|
||||||
## 安全说明
|
## 安全说明
|
||||||
|
|
||||||
@@ -113,11 +111,6 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
|
|||||||
- 邮箱:support@fit2cloud.com
|
- 邮箱:support@fit2cloud.com
|
||||||
- 电话:400-052-0755
|
- 电话:400-052-0755
|
||||||
|
|
||||||
## 致谢开源
|
|
||||||
|
|
||||||
- [Apache Guacamole](https://guacamole.apache.org/): Web 页面连接 RDP、SSH、VNC 等协议资产,JumpServer Lion 组件使用到该项目;
|
|
||||||
- [OmniDB](https://omnidb.org/): Web 页面连接使用数据库,JumpServer Web 数据库组件使用到该项目。
|
|
||||||
|
|
||||||
## License & Copyright
|
## License & Copyright
|
||||||
|
|
||||||
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.
|
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .task import *
|
from .task import *
|
||||||
from .template import *
|
from .template import *
|
||||||
|
from .virtual import *
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ __all__ = [
|
|||||||
|
|
||||||
class AccountViewSet(OrgBulkModelViewSet):
|
class AccountViewSet(OrgBulkModelViewSet):
|
||||||
model = Account
|
model = Account
|
||||||
search_fields = ('username', 'name', 'asset__name', 'asset__address')
|
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
||||||
filterset_class = AccountFilterSet
|
filterset_class = AccountFilterSet
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountSerializer,
|
'default': serializers.AccountSerializer,
|
||||||
|
'retrieve': serializers.AccountDetailSerializer,
|
||||||
}
|
}
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'partial_update': ['accounts.change_account'],
|
'partial_update': ['accounts.change_account'],
|
||||||
@@ -52,20 +53,21 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
methods=['get'], detail=False, url_path='username-suggestions',
|
methods=['post'], detail=False, url_path='username-suggestions',
|
||||||
permission_classes=[IsValidUser]
|
permission_classes=[IsValidUser]
|
||||||
)
|
)
|
||||||
def username_suggestions(self, request, *args, **kwargs):
|
def username_suggestions(self, request, *args, **kwargs):
|
||||||
asset_ids = request.query_params.get('assets')
|
asset_ids = request.data.get('assets')
|
||||||
node_keys = request.query_params.get('keys')
|
node_ids = request.data.get('nodes')
|
||||||
username = request.query_params.get('username')
|
username = request.data.get('username')
|
||||||
|
|
||||||
assets = Asset.objects.all()
|
assets = Asset.objects.all()
|
||||||
if asset_ids:
|
if asset_ids:
|
||||||
assets = assets.filter(id__in=asset_ids.split(','))
|
assets = assets.filter(id__in=asset_ids)
|
||||||
if node_keys:
|
if node_ids:
|
||||||
patten = Node.get_node_all_children_key_pattern(node_keys.split(','))
|
nodes = Node.objects.filter(id__in=node_ids)
|
||||||
assets = assets.filter(nodes__key__regex=patten)
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||||
|
assets = assets.filter(id__in=set(list(asset_ids) + list(node_asset_ids)))
|
||||||
|
|
||||||
accounts = Account.objects.filter(asset__in=assets)
|
accounts = Account.objects.filter(asset__in=assets)
|
||||||
if username:
|
if username:
|
||||||
@@ -132,11 +134,13 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
account = self.get_object()
|
account = self.get_object()
|
||||||
histories = account.history.all()
|
histories = account.history.all()
|
||||||
last_history = account.history.first()
|
latest_history = account.history.first()
|
||||||
if not last_history:
|
if not latest_history:
|
||||||
return histories
|
return histories
|
||||||
|
if account.secret != latest_history.secret:
|
||||||
if account.secret == last_history.secret \
|
return histories
|
||||||
and account.secret_type == last_history.secret_type:
|
if account.secret_type != latest_history.secret_type:
|
||||||
histories = histories.exclude(history_id=last_history.history_id)
|
return histories
|
||||||
|
histories = histories.exclude(history_id=latest_history.history_id)
|
||||||
return histories
|
return histories
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
|||||||
@action(methods=['get'], detail=False, url_path='su-from-account-templates')
|
@action(methods=['get'], detail=False, url_path='su-from-account-templates')
|
||||||
def su_from_account_templates(self, request, *args, **kwargs):
|
def su_from_account_templates(self, request, *args, **kwargs):
|
||||||
pk = request.query_params.get('template_id')
|
pk = request.query_params.get('template_id')
|
||||||
template = AccountTemplate.objects.filter(pk=pk).first()
|
templates = AccountTemplate.get_su_from_account_templates(pk)
|
||||||
templates = AccountTemplate.get_su_from_account_templates(template)
|
|
||||||
templates = self.filter_queryset(templates)
|
templates = self.filter_queryset(templates)
|
||||||
serializer = self.get_serializer(templates, many=True)
|
serializer = self.get_serializer(templates, many=True)
|
||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|||||||
20
apps/accounts/api/account/virtual.py
Normal file
20
apps/accounts/api/account/virtual.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
from accounts.models import VirtualAccount
|
||||||
|
from accounts.serializers import VirtualAccountSerializer
|
||||||
|
from common.utils import is_uuid
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualAccountViewSet(OrgBulkModelViewSet):
|
||||||
|
serializer_class = VirtualAccountSerializer
|
||||||
|
search_fields = ('alias',)
|
||||||
|
filterset_fields = ('alias',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return VirtualAccount.get_or_init_queryset()
|
||||||
|
|
||||||
|
def get_object(self, ):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
kwargs = {'pk': pk} if is_uuid(pk) else {'alias': pk}
|
||||||
|
return get_object_or_404(VirtualAccount, **kwargs)
|
||||||
@@ -26,8 +26,8 @@ class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
|||||||
|
|
||||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||||
search_fields = ('trigger',)
|
search_fields = ('trigger', 'plan__name')
|
||||||
filterset_fields = ('trigger', 'plan_id')
|
filterset_fields = ('trigger', 'plan_id', 'plan__name')
|
||||||
http_method_names = ['get', 'post', 'options']
|
http_method_names = ['get', 'post', 'options']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status, mixins, viewsets
|
from rest_framework import status, mixins, viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
@@ -95,8 +95,8 @@ class AutomationExecutionViewSet(
|
|||||||
mixins.CreateModelMixin, mixins.ListModelMixin,
|
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
search_fields = ('trigger',)
|
search_fields = ('trigger', 'automation__name')
|
||||||
filterset_fields = ('trigger', 'automation_id')
|
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||||
serializer_class = serializers.AutomationExecutionSerializer
|
serializer_class = serializers.AutomationExecutionSerializer
|
||||||
|
|
||||||
tp: str
|
tp: str
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ class AccountsConfig(AppConfig):
|
|||||||
name = 'accounts'
|
name = 'accounts'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signal_handlers
|
from . import signal_handlers # noqa
|
||||||
from . import tasks
|
from . import tasks # noqa
|
||||||
__all__ = signal_handlers
|
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from openpyxl import Workbook
|
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F
|
from openpyxl import Workbook
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import Account
|
|
||||||
from assets.const import AllTypes
|
|
||||||
from accounts.serializers import AccountSecretSerializer
|
|
||||||
from accounts.notifications import AccountBackupExecutionTaskMsg
|
from accounts.notifications import AccountBackupExecutionTaskMsg
|
||||||
from users.models import User
|
from accounts.serializers import AccountSecretSerializer
|
||||||
from common.utils import get_logger
|
from assets.const import AllTypes
|
||||||
from common.utils.timezone import local_now_display
|
|
||||||
from common.utils.file import encrypt_and_compress_zip_file
|
from common.utils.file import encrypt_and_compress_zip_file
|
||||||
|
from common.utils.timezone import local_now_display
|
||||||
logger = get_logger(__file__)
|
from users.models import User
|
||||||
|
|
||||||
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||||
|
|
||||||
@@ -76,8 +71,22 @@ class AssetAccountHandler(BaseAccountHandler):
|
|||||||
)
|
)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handler_secret(data, section):
|
||||||
|
for account_data in data:
|
||||||
|
secret = account_data.get('secret')
|
||||||
|
if not secret:
|
||||||
|
continue
|
||||||
|
length = len(secret)
|
||||||
|
index = length // 2
|
||||||
|
if section == "front":
|
||||||
|
secret = secret[:index] + '*' * (length - index)
|
||||||
|
elif section == "back":
|
||||||
|
secret = '*' * (length - index) + secret[index:]
|
||||||
|
account_data['secret'] = secret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_data_map(cls, accounts):
|
def create_data_map(cls, accounts, section):
|
||||||
data_map = defaultdict(list)
|
data_map = defaultdict(list)
|
||||||
|
|
||||||
if not accounts.exists():
|
if not accounts.exists():
|
||||||
@@ -97,9 +106,10 @@ class AssetAccountHandler(BaseAccountHandler):
|
|||||||
for tp, _accounts in account_type_map.items():
|
for tp, _accounts in account_type_map.items():
|
||||||
sheet_name = type_dict.get(tp, tp)
|
sheet_name = type_dict.get(tp, tp)
|
||||||
data = AccountSecretSerializer(_accounts, many=True).data
|
data = AccountSecretSerializer(_accounts, many=True).data
|
||||||
|
cls.handler_secret(data, section)
|
||||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||||
|
|
||||||
logger.info('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
|
print('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
|
||||||
return data_map
|
return data_map
|
||||||
|
|
||||||
|
|
||||||
@@ -109,8 +119,8 @@ class AccountBackupHandler:
|
|||||||
self.plan_name = self.execution.plan.name
|
self.plan_name = self.execution.plan.name
|
||||||
self.is_frozen = False # 任务状态冻结标志
|
self.is_frozen = False # 任务状态冻结标志
|
||||||
|
|
||||||
def create_excel(self):
|
def create_excel(self, section='complete'):
|
||||||
logger.info(
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
||||||
''
|
''
|
||||||
@@ -119,7 +129,7 @@ class AccountBackupHandler:
|
|||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
files = []
|
files = []
|
||||||
accounts = self.execution.backup_accounts
|
accounts = self.execution.backup_accounts
|
||||||
data_map = AssetAccountHandler.create_data_map(accounts)
|
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
||||||
if not data_map:
|
if not data_map:
|
||||||
return files
|
return files
|
||||||
|
|
||||||
@@ -133,14 +143,14 @@ class AccountBackupHandler:
|
|||||||
wb.save(filename)
|
wb.save(filename)
|
||||||
files.append(filename)
|
files.append(filename)
|
||||||
timedelta = round((time.time() - time_start), 2)
|
timedelta = round((time.time() - time_start), 2)
|
||||||
logger.info('步骤完成: 用时 {}s'.format(timedelta))
|
print('步骤完成: 用时 {}s'.format(timedelta))
|
||||||
return files
|
return files
|
||||||
|
|
||||||
def send_backup_mail(self, files, recipients):
|
def send_backup_mail(self, files, recipients):
|
||||||
if not files:
|
if not files:
|
||||||
return
|
return
|
||||||
recipients = User.objects.filter(id__in=list(recipients))
|
recipients = User.objects.filter(id__in=list(recipients))
|
||||||
logger.info(
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 发送备份邮件\033[0m'
|
'\033[32m>>> 发送备份邮件\033[0m'
|
||||||
''
|
''
|
||||||
@@ -155,7 +165,7 @@ class AccountBackupHandler:
|
|||||||
encrypt_and_compress_zip_file(attachment, password, files)
|
encrypt_and_compress_zip_file(attachment, password, files)
|
||||||
attachment_list = [attachment, ]
|
attachment_list = [attachment, ]
|
||||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
||||||
logger.info('邮件已发送至{}({})'.format(user, user.email))
|
print('邮件已发送至{}({})'.format(user, user.email))
|
||||||
for file in files:
|
for file in files:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
|
|
||||||
@@ -163,33 +173,42 @@ class AccountBackupHandler:
|
|||||||
self.execution.reason = reason[:1024]
|
self.execution.reason = reason[:1024]
|
||||||
self.execution.is_success = is_success
|
self.execution.is_success = is_success
|
||||||
self.execution.save()
|
self.execution.save()
|
||||||
logger.info('已完成对任务状态的更新')
|
print('已完成对任务状态的更新')
|
||||||
|
|
||||||
def step_finished(self, is_success):
|
@staticmethod
|
||||||
|
def step_finished(is_success):
|
||||||
if is_success:
|
if is_success:
|
||||||
logger.info('任务执行成功')
|
print('任务执行成功')
|
||||||
else:
|
else:
|
||||||
logger.error('任务执行失败')
|
print('任务执行失败')
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
is_success = False
|
is_success = False
|
||||||
error = '-'
|
error = '-'
|
||||||
try:
|
try:
|
||||||
recipients = self.execution.plan_snapshot.get('recipients')
|
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||||
if not recipients:
|
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||||
logger.info(
|
if not recipients_part_one and not recipients_part_two:
|
||||||
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 该备份任务未分配收件人\033[0m'
|
'\033[32m>>> 该备份任务未分配收件人\033[0m'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
if recipients_part_one and recipients_part_two:
|
||||||
|
files = self.create_excel(section='front')
|
||||||
|
self.send_backup_mail(files, recipients_part_one)
|
||||||
|
|
||||||
|
files = self.create_excel(section='back')
|
||||||
|
self.send_backup_mail(files, recipients_part_two)
|
||||||
else:
|
else:
|
||||||
|
recipients = recipients_part_one or recipients_part_two
|
||||||
files = self.create_excel()
|
files = self.create_excel()
|
||||||
self.send_backup_mail(files, recipients)
|
self.send_backup_mail(files, recipients)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.is_frozen = True
|
self.is_frozen = True
|
||||||
logger.error('任务执行被异常中断')
|
print('任务执行被异常中断')
|
||||||
logger.info('下面打印发生异常的 Traceback 信息 : ')
|
print('下面打印发生异常的 Traceback 信息 : ')
|
||||||
logger.error(e, exc_info=True)
|
print(e)
|
||||||
error = str(e)
|
error = str(e)
|
||||||
else:
|
else:
|
||||||
is_success = True
|
is_success = True
|
||||||
@@ -199,15 +218,15 @@ class AccountBackupHandler:
|
|||||||
self.step_finished(is_success)
|
self.step_finished(is_success)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
logger.info('任务开始: {}'.format(local_now_display()))
|
print('任务开始: {}'.format(local_now_display()))
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
try:
|
try:
|
||||||
self._run()
|
self._run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('任务运行出现异常')
|
print('任务运行出现异常')
|
||||||
logger.error('下面显示异常 Traceback 信息: ')
|
print('下面显示异常 Traceback 信息: ')
|
||||||
logger.error(e, exc_info=True)
|
print(e)
|
||||||
finally:
|
finally:
|
||||||
logger.info('\n任务结束: {}'.format(local_now_display()))
|
print('\n任务结束: {}'.format(local_now_display()))
|
||||||
timedelta = round((time.time() - time_start), 2)
|
timedelta = round((time.time() - time_start), 2)
|
||||||
logger.info('用时: {}'.format(timedelta))
|
print('用时: {}'.format(timedelta))
|
||||||
|
|||||||
@@ -4,13 +4,9 @@ import time
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
|
|
||||||
from .handlers import AccountBackupHandler
|
from .handlers import AccountBackupHandler
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupManager:
|
class AccountBackupManager:
|
||||||
def __init__(self, execution):
|
def __init__(self, execution):
|
||||||
@@ -23,7 +19,7 @@ class AccountBackupManager:
|
|||||||
|
|
||||||
def do_run(self):
|
def do_run(self):
|
||||||
execution = self.execution
|
execution = self.execution
|
||||||
logger.info('\n\033[33m# 账号备份计划正在执行\033[0m')
|
print('\n\033[33m# 账号备份计划正在执行\033[0m')
|
||||||
handler = AccountBackupHandler(execution)
|
handler = AccountBackupHandler(execution)
|
||||||
handler.run()
|
handler.run()
|
||||||
|
|
||||||
@@ -35,10 +31,10 @@ class AccountBackupManager:
|
|||||||
self.time_end = time.time()
|
self.time_end = time.time()
|
||||||
self.date_end = timezone.now()
|
self.date_end = timezone.now()
|
||||||
|
|
||||||
logger.info('\n\n' + '-' * 80)
|
print('\n\n' + '-' * 80)
|
||||||
logger.info('计划执行结束 {}\n'.format(local_now_display()))
|
print('计划执行结束 {}\n'.format(local_now_display()))
|
||||||
self.timedelta = self.time_end - self.time_start
|
self.timedelta = self.time_end - self.time_start
|
||||||
logger.info('用时: {}s'.format(self.timedelta))
|
print('用时: {}s'.format(self.timedelta))
|
||||||
self.execution.timedelta = self.timedelta
|
self.execution.timedelta = self.timedelta
|
||||||
self.execution.save()
|
self.execution.save()
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: Test privileged account (paramiko)
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
@@ -12,9 +13,14 @@
|
|||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
register: ping_info
|
register: ping_info
|
||||||
|
|
||||||
- name: Change asset password
|
- name: Change asset password (paramiko)
|
||||||
custom_command:
|
custom_command:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
@@ -22,6 +28,11 @@
|
|||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
commands: "{{ params.commands }}"
|
commands: "{{ params.commands }}"
|
||||||
@@ -30,9 +41,10 @@
|
|||||||
when: ping_info is succeeded
|
when: ping_info is succeeded
|
||||||
register: change_info
|
register: change_info
|
||||||
|
|
||||||
- name: Verify password
|
- name: Verify password (paramiko)
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
login_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
become: false
|
||||||
|
|||||||
@@ -1,10 +1,41 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Change password
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
shell: "{{ params.shell }}"
|
||||||
|
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
expires: -1
|
||||||
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} group"
|
||||||
|
ansible.builtin.group:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user to group"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('des') }}"
|
password: "{{ account.secret | password_hash('des') }}"
|
||||||
@@ -12,44 +43,54 @@
|
|||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
|
||||||
- name: create user If it already exists, no operation will be performed
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ account.username }}"
|
|
||||||
when: account.secret_type == "ssh_key"
|
|
||||||
|
|
||||||
- name: remove jumpserver ssh key
|
- name: remove jumpserver ssh key
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: "{{ ssh_params.dest }}"
|
dest: "{{ ssh_params.dest }}"
|
||||||
regexp: "{{ ssh_params.regexp }}"
|
regexp: "{{ ssh_params.regexp }}"
|
||||||
state: absent
|
state: absent
|
||||||
when:
|
when:
|
||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Change SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
dest: /etc/sudoers
|
||||||
|
state: present
|
||||||
|
regexp: "^{{ account.username }} ALL="
|
||||||
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
|
validate: visudo -cf %s
|
||||||
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
|||||||
@@ -4,9 +4,58 @@ category: host
|
|||||||
type:
|
type:
|
||||||
- AIX
|
- AIX
|
||||||
method: change_secret
|
method: change_secret
|
||||||
|
params:
|
||||||
|
- name: sudo
|
||||||
|
type: str
|
||||||
|
label: 'Sudo'
|
||||||
|
default: '/bin/whoami'
|
||||||
|
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||||
|
|
||||||
|
- name: shell
|
||||||
|
type: str
|
||||||
|
label: 'Shell'
|
||||||
|
default: '/bin/bash'
|
||||||
|
|
||||||
|
- name: home
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params home label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params home help text' | trans }}"
|
||||||
|
|
||||||
|
- name: groups
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params groups label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params groups help text' | trans }}"
|
||||||
|
|
||||||
i18n:
|
i18n:
|
||||||
AIX account change secret:
|
AIX account change secret:
|
||||||
zh: 使用 Ansible 模块 user 执行账号改密 (DES)
|
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
|
||||||
ja: Ansible user モジュールを使用してアカウントのパスワード変更 (DES)
|
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
|
||||||
en: Using Ansible module user to change account secret (DES)
|
en: 'Using Ansible module user to change account secret (DES)'
|
||||||
|
|
||||||
|
Params sudo help text:
|
||||||
|
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||||
|
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||||
|
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||||
|
|
||||||
|
Params home help text:
|
||||||
|
zh: '默认家目录 /home/{账号用户名}'
|
||||||
|
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||||
|
en: 'Default home directory /home/{account username}'
|
||||||
|
|
||||||
|
Params groups help text:
|
||||||
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||||
|
|
||||||
|
Params home label:
|
||||||
|
zh: '家目录'
|
||||||
|
ja: 'ホームディレクトリ'
|
||||||
|
en: 'Home'
|
||||||
|
|
||||||
|
Params groups label:
|
||||||
|
zh: '用户组'
|
||||||
|
ja: 'グループ'
|
||||||
|
en: 'Groups'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,41 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Change password
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
shell: "{{ params.shell }}"
|
||||||
|
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
expires: -1
|
||||||
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} group"
|
||||||
|
ansible.builtin.group:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user to group"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('sha512') }}"
|
password: "{{ account.secret | password_hash('sha512') }}"
|
||||||
@@ -12,11 +43,6 @@
|
|||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
|
||||||
- name: create user If it already exists, no operation will be performed
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ account.username }}"
|
|
||||||
when: account.secret_type == "ssh_key"
|
|
||||||
|
|
||||||
- name: remove jumpserver ssh key
|
- name: remove jumpserver ssh key
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: "{{ ssh_params.dest }}"
|
dest: "{{ ssh_params.dest }}"
|
||||||
@@ -26,30 +52,45 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Change SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
dest: /etc/sudoers
|
||||||
|
state: present
|
||||||
|
regexp: "^{{ account.username }} ALL="
|
||||||
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
|
validate: visudo -cf %s
|
||||||
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
|||||||
@@ -5,9 +5,59 @@ type:
|
|||||||
- unix
|
- unix
|
||||||
- linux
|
- linux
|
||||||
method: change_secret
|
method: change_secret
|
||||||
|
params:
|
||||||
|
- name: sudo
|
||||||
|
type: str
|
||||||
|
label: 'Sudo'
|
||||||
|
default: '/bin/whoami'
|
||||||
|
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||||
|
|
||||||
|
- name: shell
|
||||||
|
type: str
|
||||||
|
label: 'Shell'
|
||||||
|
default: '/bin/bash'
|
||||||
|
help_text: ''
|
||||||
|
|
||||||
|
- name: home
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params home label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params home help text' | trans }}"
|
||||||
|
|
||||||
|
- name: groups
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params groups label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params groups help text' | trans }}"
|
||||||
|
|
||||||
i18n:
|
i18n:
|
||||||
Posix account change secret:
|
Posix account change secret:
|
||||||
zh: 使用 Ansible 模块 user 执行账号改密 (SHA512)
|
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
|
||||||
ja: Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)
|
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
|
||||||
en: Using Ansible module user to change account secret (SHA512)
|
en: 'Using Ansible module user to change account secret (SHA512)'
|
||||||
|
|
||||||
|
Params sudo help text:
|
||||||
|
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||||
|
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||||
|
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||||
|
|
||||||
|
Params home help text:
|
||||||
|
zh: '默认家目录 /home/{账号用户名}'
|
||||||
|
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||||
|
en: 'Default home directory /home/{account username}'
|
||||||
|
|
||||||
|
Params groups help text:
|
||||||
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||||
|
|
||||||
|
Params home label:
|
||||||
|
zh: '家目录'
|
||||||
|
ja: 'ホームディレクトリ'
|
||||||
|
en: 'Home'
|
||||||
|
|
||||||
|
Params groups label:
|
||||||
|
zh: '用户组'
|
||||||
|
ja: 'グループ'
|
||||||
|
en: 'Groups'
|
||||||
|
|
||||||
|
|||||||
@@ -8,17 +8,13 @@
|
|||||||
# debug:
|
# debug:
|
||||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||||
|
|
||||||
|
|
||||||
- name: Get groups of a Windows user
|
|
||||||
ansible.windows.win_user:
|
|
||||||
name: "{{ jms_account.username }}"
|
|
||||||
register: user_info
|
|
||||||
|
|
||||||
- name: Change password
|
- name: Change password
|
||||||
ansible.windows.win_user:
|
ansible.windows.win_user:
|
||||||
|
fullname: "{{ account.username}}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
groups: "{{ user_info.groups[0].name }}"
|
password_never_expires: yes
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
groups_action: add
|
groups_action: add
|
||||||
update_password: always
|
update_password: always
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
|
|||||||
@@ -5,9 +5,22 @@ method: change_secret
|
|||||||
category: host
|
category: host
|
||||||
type:
|
type:
|
||||||
- windows
|
- windows
|
||||||
|
params:
|
||||||
|
- name: groups
|
||||||
|
type: str
|
||||||
|
label: '用户组'
|
||||||
|
default: 'Users,Remote Desktop Users'
|
||||||
|
help_text: "{{ 'Params groups help text' | trans }}"
|
||||||
|
|
||||||
|
|
||||||
i18n:
|
i18n:
|
||||||
Windows account change secret:
|
Windows account change secret:
|
||||||
zh: 使用 Ansible 模块 win_user 执行 Windows 账号改密
|
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密'
|
||||||
ja: Ansible win_user モジュールを使用して Windows アカウントのパスワード変更
|
ja: 'Ansible win_user モジュールを使用して Windows アカウントのパスワード変更'
|
||||||
en: Using Ansible module win_user to change Windows account secret
|
en: 'Using Ansible module win_user to change Windows account secret'
|
||||||
|
|
||||||
|
Params groups help text:
|
||||||
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Push user
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
shell: "{{ params.shell }}"
|
shell: "{{ params.shell }}"
|
||||||
@@ -12,22 +19,26 @@
|
|||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: "Add {{ account.username }} group"
|
- name: "Add {{ account.username }} group"
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: Add user groups
|
- name: "Add {{ account.username }} user to group"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
when: params.groups
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
- name: Push user password
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('sha512') }}"
|
password: "{{ account.secret | password_hash('des') }}"
|
||||||
update_password: always
|
update_password: always
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
@@ -41,14 +52,14 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Push SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
- name: Set sudo setting
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: /etc/sudoers
|
dest: /etc/sudoers
|
||||||
state: present
|
state: present
|
||||||
@@ -56,25 +67,31 @@
|
|||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
|
- user_info.failed
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Push user
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
shell: "{{ params.shell }}"
|
shell: "{{ params.shell }}"
|
||||||
@@ -12,19 +19,23 @@
|
|||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: "Add {{ account.username }} group"
|
- name: "Add {{ account.username }} group"
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: Add user groups
|
- name: "Add {{ account.username }} user to group"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
when: params.groups
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
- name: Push user password
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('sha512') }}"
|
password: "{{ account.secret | password_hash('sha512') }}"
|
||||||
@@ -41,14 +52,14 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Push SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
- name: Set sudo setting
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: /etc/sudoers
|
dest: /etc/sudoers
|
||||||
state: present
|
state: present
|
||||||
@@ -56,25 +67,31 @@
|
|||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
|
- user_info.failed
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
- hosts: custom
|
- hosts: custom
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
|
ansible_shell_type: sh
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account (pyfreerdp)
|
||||||
ssh_ping:
|
rdp_ping:
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account (paramiko)
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
@@ -12,3 +13,8 @@
|
|||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_secret_type: "{{ account.secret_type }}"
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
login_private_key_path: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
|
|||||||
41
apps/accounts/backends/__init__.py
Normal file
41
apps/accounts/backends/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.utils.functional import LazyObject
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
from ..const import VaultTypeChoices
|
||||||
|
|
||||||
|
__all__ = ['vault_client', 'get_vault_client']
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vault_client(raise_exception=False, **kwargs):
|
||||||
|
enabled = kwargs.get('VAULT_ENABLED')
|
||||||
|
tp = 'hcp' if enabled else 'local'
|
||||||
|
try:
|
||||||
|
module_path = f'apps.accounts.backends.{tp}.main'
|
||||||
|
client = import_module(module_path).Vault(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Init vault client failed: {e}')
|
||||||
|
if raise_exception:
|
||||||
|
raise
|
||||||
|
tp = VaultTypeChoices.local
|
||||||
|
module_path = f'apps.accounts.backends.{tp}.main'
|
||||||
|
client = import_module(module_path).Vault(**kwargs)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
class VaultClient(LazyObject):
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
from jumpserver import settings as js_settings
|
||||||
|
from django.conf import settings
|
||||||
|
vault_config_names = [k for k in js_settings.__dict__.keys() if k.startswith('VAULT_')]
|
||||||
|
vault_configs = {name: getattr(settings, name, None) for name in vault_config_names}
|
||||||
|
self._wrapped = get_vault_client(**vault_configs)
|
||||||
|
|
||||||
|
|
||||||
|
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
|
||||||
|
vault_client = VaultClient()
|
||||||
74
apps/accounts/backends/base.py
Normal file
74
apps/accounts/backends/base.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
|
__all__ = ['BaseVault']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVault(ABC):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.enabled = kwargs.get('VAULT_ENABLED')
|
||||||
|
|
||||||
|
def get(self, instance):
|
||||||
|
""" 返回 secret 值 """
|
||||||
|
return self._get(instance)
|
||||||
|
|
||||||
|
def create(self, instance):
|
||||||
|
if not instance.secret_has_save_to_vault:
|
||||||
|
self._create(instance)
|
||||||
|
self._clean_db_secret(instance)
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
if instance.is_sync_metadata:
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
def update(self, instance):
|
||||||
|
if not instance.secret_has_save_to_vault:
|
||||||
|
self._update(instance)
|
||||||
|
self._clean_db_secret(instance)
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
if instance.is_sync_metadata:
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
def delete(self, instance):
|
||||||
|
self._delete(instance)
|
||||||
|
|
||||||
|
def save_metadata(self, instance):
|
||||||
|
metadata = model_to_dict(instance, fields=[
|
||||||
|
'name', 'username', 'secret_type',
|
||||||
|
'connectivity', 'su_from', 'privileged'
|
||||||
|
])
|
||||||
|
metadata = {k: str(v)[:500] for k, v in metadata.items() if v}
|
||||||
|
return self._save_metadata(instance, metadata)
|
||||||
|
|
||||||
|
# -------- abstractmethod -------- #
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _get(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _create(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _update(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _delete(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_active(self, *args, **kwargs) -> (bool, str):
|
||||||
|
raise NotImplementedError
|
||||||
1
apps/accounts/backends/hcp/__init__.py
Normal file
1
apps/accounts/backends/hcp/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .main import *
|
||||||
84
apps/accounts/backends/hcp/entries.py
Normal file
84
apps/accounts/backends/hcp/entries.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import sys
|
||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
from common.db.utils import Encryptor
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
|
||||||
|
current_module = sys.modules[__name__]
|
||||||
|
|
||||||
|
__all__ = ['build_entry']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEntry(ABC):
|
||||||
|
|
||||||
|
def __init__(self, instance):
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def full_path(self):
|
||||||
|
path_base = self.path_base
|
||||||
|
path_spec = self.path_spec
|
||||||
|
path = f'{path_base}/{path_spec}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_base(self):
|
||||||
|
path = f'orgs/{self.instance.org_id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_internal_data(self):
|
||||||
|
secret = getattr(self.instance, '_secret', None)
|
||||||
|
if secret is not None:
|
||||||
|
secret = Encryptor(secret).encrypt()
|
||||||
|
data = {'secret': secret}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_external_data(data):
|
||||||
|
secret = data.pop('secret', None)
|
||||||
|
if secret is not None:
|
||||||
|
secret = Encryptor(secret).decrypt()
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
class AccountEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'assets/{self.instance.asset_id}/accounts/{self.instance.id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTemplateEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'account-templates/{self.instance.id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class HistoricalAccountEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_base(self):
|
||||||
|
account = self.instance.instance
|
||||||
|
path = f'accounts/{account.id}/'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'histories/{self.instance.history_id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def build_entry(instance) -> BaseEntry:
|
||||||
|
class_name = instance.__class__.__name__
|
||||||
|
entry_class_name = f'{class_name}Entry'
|
||||||
|
entry_class = getattr(current_module, entry_class_name, None)
|
||||||
|
if not entry_class:
|
||||||
|
raise Exception(f'Entry class {entry_class_name} is not found')
|
||||||
|
return entry_class(instance)
|
||||||
53
apps/accounts/backends/hcp/main.py
Normal file
53
apps/accounts/backends/hcp/main.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from common.db.utils import get_logger
|
||||||
|
from .entries import build_entry
|
||||||
|
from .service import VaultKVClient
|
||||||
|
from ..base import BaseVault
|
||||||
|
|
||||||
|
__all__ = ['Vault']
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Vault(BaseVault):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.client = VaultKVClient(
|
||||||
|
url=kwargs.get('VAULT_HCP_HOST'),
|
||||||
|
token=kwargs.get('VAULT_HCP_TOKEN'),
|
||||||
|
mount_point=kwargs.get('VAULT_HCP_MOUNT_POINT')
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self.client.is_active()
|
||||||
|
|
||||||
|
def _get(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
# TODO: get data 是不是层数太多了
|
||||||
|
data = self.client.get(path=entry.full_path).get('data', {})
|
||||||
|
data = entry.to_external_data(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _create(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
data = entry.to_internal_data()
|
||||||
|
self.client.create(path=entry.full_path, data=data)
|
||||||
|
|
||||||
|
def _update(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
data = entry.to_internal_data()
|
||||||
|
self.client.patch(path=entry.full_path, data=data)
|
||||||
|
|
||||||
|
def _delete(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
self.client.delete(path=entry.full_path)
|
||||||
|
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
instance.is_sync_metadata = False
|
||||||
|
instance.mark_secret_save_to_vault()
|
||||||
|
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
try:
|
||||||
|
entry = build_entry(instance)
|
||||||
|
self.client.update_metadata(path=entry.full_path, metadata=metadata)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'save metadata error: {e}')
|
||||||
102
apps/accounts/backends/hcp/service.py
Normal file
102
apps/accounts/backends/hcp/service.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
import hvac
|
||||||
|
from hvac import exceptions
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ['VaultKVClient']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKVClient(object):
|
||||||
|
max_versions = 20
|
||||||
|
|
||||||
|
def __init__(self, url, token, mount_point):
|
||||||
|
assert isinstance(self.max_versions, int) and self.max_versions >= 3, (
|
||||||
|
'max_versions must to be an integer that is greater than or equal to 3'
|
||||||
|
)
|
||||||
|
self.client = hvac.Client(url=url, token=token)
|
||||||
|
self.mount_point = mount_point
|
||||||
|
self.enable_secrets_engine_if_need()
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
try:
|
||||||
|
if not self.client.sys.is_initialized():
|
||||||
|
return False, 'Vault is not initialized'
|
||||||
|
if self.client.sys.is_sealed():
|
||||||
|
return False, 'Vault is sealed'
|
||||||
|
if not self.client.is_authenticated():
|
||||||
|
return False, 'Vault is not authenticated'
|
||||||
|
except ConnectionError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
return False, f'Vault is not reachable: {e}'
|
||||||
|
else:
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
def enable_secrets_engine_if_need(self):
|
||||||
|
secrets_engines = self.client.sys.list_mounted_secrets_engines()
|
||||||
|
mount_points = secrets_engines.keys()
|
||||||
|
if f'{self.mount_point}/' in mount_points:
|
||||||
|
return
|
||||||
|
self.client.sys.enable_secrets_engine(
|
||||||
|
backend_type='kv',
|
||||||
|
path=self.mount_point,
|
||||||
|
options={'version': 2} # TODO: version 是否从配置中读取?
|
||||||
|
)
|
||||||
|
self.client.secrets.kv.v2.configure(
|
||||||
|
max_versions=self.max_versions,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, path, version=None):
|
||||||
|
try:
|
||||||
|
response = self.client.secrets.kv.v2.read_secret_version(
|
||||||
|
path=path,
|
||||||
|
version=version,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
except exceptions.InvalidPath as e:
|
||||||
|
return {}
|
||||||
|
data = response.get('data', {})
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, path, data: dict):
|
||||||
|
self._update_or_create(path=path, data=data)
|
||||||
|
|
||||||
|
def update(self, path, data: dict):
|
||||||
|
""" 未更新的数据会被删除 """
|
||||||
|
self._update_or_create(path=path, data=data)
|
||||||
|
|
||||||
|
def patch(self, path, data: dict):
|
||||||
|
""" 未更新的数据不会被删除 """
|
||||||
|
self.client.secrets.kv.v2.patch(
|
||||||
|
path=path,
|
||||||
|
secret=data,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, path):
|
||||||
|
self.client.secrets.kv.v2.delete_metadata_and_all_versions(
|
||||||
|
path=path,
|
||||||
|
mount_point=self.mount_point,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_or_create(self, path, data: dict):
|
||||||
|
self.client.secrets.kv.v2.create_or_update_secret(
|
||||||
|
path=path,
|
||||||
|
secret=data,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_metadata(self, path, metadata: dict):
|
||||||
|
try:
|
||||||
|
self.client.secrets.kv.v2.update_metadata(
|
||||||
|
path=path,
|
||||||
|
mount_point=self.mount_point,
|
||||||
|
custom_metadata=metadata
|
||||||
|
)
|
||||||
|
except exceptions.InvalidPath as e:
|
||||||
|
logger.error('Update metadata error: {}'.format(e))
|
||||||
1
apps/accounts/backends/local/__init__.py
Normal file
1
apps/accounts/backends/local/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .main import *
|
||||||
36
apps/accounts/backends/local/main.py
Normal file
36
apps/accounts/backends/local/main.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from common.utils import get_logger
|
||||||
|
from ..base import BaseVault
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ['Vault']
|
||||||
|
|
||||||
|
|
||||||
|
class Vault(BaseVault):
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
def _get(self, instance):
|
||||||
|
secret = getattr(instance, '_secret', None)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
def _create(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _update(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _delete(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
""" Ignore *重要* 不能删除本地 secret """
|
||||||
|
pass
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .automation import *
|
from .automation import *
|
||||||
|
from .vault import *
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class SecretType(TextChoices):
|
class SecretType(TextChoices):
|
||||||
@@ -7,12 +7,18 @@ class SecretType(TextChoices):
|
|||||||
SSH_KEY = 'ssh_key', _('SSH key')
|
SSH_KEY = 'ssh_key', _('SSH key')
|
||||||
ACCESS_KEY = 'access_key', _('Access key')
|
ACCESS_KEY = 'access_key', _('Access key')
|
||||||
TOKEN = 'token', _('Token')
|
TOKEN = 'token', _('Token')
|
||||||
|
API_KEY = 'api_key', _("API key")
|
||||||
|
|
||||||
|
|
||||||
class AliasAccount(TextChoices):
|
class AliasAccount(TextChoices):
|
||||||
ALL = '@ALL', _('All')
|
ALL = '@ALL', _('All')
|
||||||
INPUT = '@INPUT', _('Manual input')
|
INPUT = '@INPUT', _('Manual input')
|
||||||
USER = '@USER', _('Dynamic user')
|
USER = '@USER', _('Dynamic user')
|
||||||
|
ANON = '@ANON', _('Anonymous account')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def virtual_choices(cls):
|
||||||
|
return [(k, v) for k, v in cls.choices if k not in (cls.ALL,)]
|
||||||
|
|
||||||
|
|
||||||
class Source(TextChoices):
|
class Source(TextChoices):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import Connectivity
|
from assets.const import Connectivity
|
||||||
from common.db.fields import TreeChoices
|
from common.db.fields import TreeChoices
|
||||||
|
|||||||
9
apps/accounts/const/vault.py
Normal file
9
apps/accounts/const/vault.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
__all__ = ['VaultTypeChoices']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultTypeChoices(models.TextChoices):
|
||||||
|
local = 'local', _('Database')
|
||||||
|
hcp = 'hcp', _('HCP Vault')
|
||||||
@@ -13,7 +13,8 @@ class AccountFilterSet(BaseFilterSet):
|
|||||||
hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact')
|
hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact')
|
||||||
username = drf_filters.CharFilter(field_name="username", lookup_expr='exact')
|
username = drf_filters.CharFilter(field_name="username", lookup_expr='exact')
|
||||||
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact')
|
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact')
|
||||||
asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact')
|
asset_id = drf_filters.CharFilter(field_name="asset", lookup_expr='exact')
|
||||||
|
asset = drf_filters.CharFilter(field_name='asset', lookup_expr='exact')
|
||||||
assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
|
assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
|
||||||
nodes = drf_filters.CharFilter(method='filter_nodes')
|
nodes = drf_filters.CharFilter(method='filter_nodes')
|
||||||
node_id = drf_filters.CharFilter(method='filter_nodes')
|
node_id = drf_filters.CharFilter(method='filter_nodes')
|
||||||
@@ -45,7 +46,7 @@ class AccountFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = ['id', 'asset_id', 'source_id']
|
fields = ['id', 'asset', 'source_id', 'secret_type']
|
||||||
|
|
||||||
|
|
||||||
class GatheredAccountFilterSet(BaseFilterSet):
|
class GatheredAccountFilterSet(BaseFilterSet):
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# Generated by Django 3.2.14 on 2022-12-28 07:29
|
# Generated by Django 3.2.14 on 2022-12-28 07:29
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import simple_history.models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
import common.db.encoder
|
import common.db.encoder
|
||||||
import common.db.fields
|
import common.db.fields
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import simple_history.models
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -29,13 +31,16 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
('org_id',
|
('org_id',
|
||||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||||
('connectivity', models.CharField(choices=[('-', 'Unknown'), ('ok', 'Ok'), ('err', 'Error')], default='-', max_length=16, verbose_name='Connectivity')),
|
('connectivity',
|
||||||
|
models.CharField(choices=[('-', 'Unknown'), ('ok', 'Ok'), ('err', 'Error')], default='-',
|
||||||
|
max_length=16, verbose_name='Connectivity')),
|
||||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||||
('secret_type', models.CharField(
|
('secret_type', models.CharField(
|
||||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||||
|
verbose_name='Secret type')),
|
||||||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||||
@@ -61,7 +66,8 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||||
('secret_type', models.CharField(
|
('secret_type', models.CharField(
|
||||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||||
|
verbose_name='Secret type')),
|
||||||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||||
('version', models.IntegerField(default=0, verbose_name='Version')),
|
('version', models.IntegerField(default=0, verbose_name='Version')),
|
||||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
@@ -96,7 +102,8 @@ class Migration(migrations.Migration):
|
|||||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||||
('secret_type', models.CharField(
|
('secret_type', models.CharField(
|
||||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||||
|
verbose_name='Secret type')),
|
||||||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Generated by Django 3.2.16 on 2022-12-30 08:08
|
# Generated by Django 3.2.16 on 2022-12-30 08:08
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
import common.db.encoder
|
import common.db.encoder
|
||||||
import common.db.fields
|
import common.db.fields
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -53,7 +55,8 @@ class Migration(migrations.Migration):
|
|||||||
primary_key=True, serialize=False, to='assets.baseautomation')),
|
primary_key=True, serialize=False, to='assets.baseautomation')),
|
||||||
('secret_type', models.CharField(
|
('secret_type', models.CharField(
|
||||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||||
|
verbose_name='Secret type')),
|
||||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
|
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
|
||||||
('random_one', 'All assets use the same random password'),
|
('random_one', 'All assets use the same random password'),
|
||||||
('random_all',
|
('random_all',
|
||||||
@@ -110,7 +113,7 @@ class Migration(migrations.Migration):
|
|||||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
|
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
|
||||||
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
|
||||||
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
||||||
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
||||||
('status', models.CharField(default='pending', max_length=16)),
|
('status', models.CharField(default='pending', max_length=16)),
|
||||||
@@ -156,7 +159,8 @@ class Migration(migrations.Migration):
|
|||||||
primary_key=True, serialize=False, to='assets.baseautomation')),
|
primary_key=True, serialize=False, to='assets.baseautomation')),
|
||||||
('secret_type', models.CharField(
|
('secret_type', models.CharField(
|
||||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||||
|
verbose_name='Secret type')),
|
||||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
|
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
|
||||||
('random_one', 'All assets use the same random password'),
|
('random_one', 'All assets use the same random password'),
|
||||||
('random_all',
|
('random_all',
|
||||||
|
|||||||
28
apps/accounts/migrations/0012_auto_20230621_1456.py
Normal file
28
apps/accounts/migrations/0012_auto_20230621_1456.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-06-21 06:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0011_auto_20230506_1443'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='account',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='historicalaccount',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
]
|
||||||
77
apps/accounts/migrations/0013_account_backup_recipients.py
Normal file
77
apps/accounts/migrations/0013_account_backup_recipients.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-08-03 08:28
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import common.db.encoder
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_recipients(apps, schema_editor):
|
||||||
|
account_backup_model = apps.get_model('accounts', 'AccountBackupAutomation')
|
||||||
|
execution_model = apps.get_model('accounts', 'AccountBackupExecution')
|
||||||
|
for account_backup in account_backup_model.objects.all():
|
||||||
|
recipients = list(account_backup.recipients.all())
|
||||||
|
if not recipients:
|
||||||
|
continue
|
||||||
|
account_backup.recipients_part_one.set(recipients)
|
||||||
|
|
||||||
|
objs = []
|
||||||
|
for execution in execution_model.objects.all():
|
||||||
|
snapshot = execution.snapshot
|
||||||
|
recipients = snapshot.pop('recipients', {})
|
||||||
|
snapshot.update({'recipients_part_one': recipients, 'recipients_part_two': {}})
|
||||||
|
objs.append(execution)
|
||||||
|
execution_model.objects.bulk_update(objs, ['snapshot'])
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_snapshot(apps, schema_editor):
|
||||||
|
model = apps.get_model('accounts', 'AccountBackupExecution')
|
||||||
|
objs = []
|
||||||
|
for execution in model.objects.all():
|
||||||
|
execution.snapshot = execution.plan_snapshot
|
||||||
|
objs.append(execution)
|
||||||
|
model.objects.bulk_update(objs, ['snapshot'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('accounts', '0012_auto_20230621_1456'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='recipients_part_one',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name='recipient_part_one_plans',
|
||||||
|
to=settings.AUTH_USER_MODEL, verbose_name='Recipient part one'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='recipients_part_two',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name='recipient_part_two_plans',
|
||||||
|
to=settings.AUTH_USER_MODEL, verbose_name='Recipient part two'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupexecution',
|
||||||
|
name='snapshot',
|
||||||
|
field=models.JSONField(
|
||||||
|
default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder,
|
||||||
|
null=True, blank=True, verbose_name='Account backup snapshot'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_snapshot),
|
||||||
|
migrations.RunPython(migrate_recipients),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='accountbackupexecution',
|
||||||
|
name='plan_snapshot',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='recipients',
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
||||||
30
apps/accounts/migrations/0014_virtualaccount.py
Normal file
30
apps/accounts/migrations/0014_virtualaccount.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-08-01 09:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0013_account_backup_recipients'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VirtualAccount',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||||
|
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account')], max_length=128, verbose_name='Alias')),
|
||||||
|
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('alias', 'org_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
from .base import *
|
|
||||||
from .account import *
|
from .account import *
|
||||||
from .automations import *
|
from .automations import *
|
||||||
|
from .base import *
|
||||||
|
from .template import *
|
||||||
|
from .virtual import *
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Q
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
from assets.models.base import AbsConnectivity
|
from assets.models.base import AbsConnectivity
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from .base import BaseAccount
|
from .base import BaseAccount
|
||||||
from ..const import AliasAccount, Source
|
from .mixins import VaultModelMixin
|
||||||
|
from ..const import Source
|
||||||
|
|
||||||
__all__ = ['Account', 'AccountTemplate']
|
__all__ = ['Account', 'AccountHistoricalRecords']
|
||||||
|
|
||||||
|
|
||||||
class AccountHistoricalRecords(HistoricalRecords):
|
class AccountHistoricalRecords(HistoricalRecords):
|
||||||
@@ -32,7 +31,7 @@ class AccountHistoricalRecords(HistoricalRecords):
|
|||||||
diff = attrs - history_attrs
|
diff = attrs - history_attrs
|
||||||
if not diff:
|
if not diff:
|
||||||
return
|
return
|
||||||
super().post_save(instance, created, using=using, **kwargs)
|
return super().post_save(instance, created, using=using, **kwargs)
|
||||||
|
|
||||||
def create_history_model(self, model, inherited):
|
def create_history_model(self, model, inherited):
|
||||||
if self.included_fields and not self.excluded_fields:
|
if self.included_fields and not self.excluded_fields:
|
||||||
@@ -53,7 +52,7 @@ class Account(AbsConnectivity, BaseAccount):
|
|||||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||||
)
|
)
|
||||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||||
|
|
||||||
@@ -88,97 +87,28 @@ class Account(AbsConnectivity, BaseAccount):
|
|||||||
def has_secret(self):
|
def has_secret(self):
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_manual_account(cls):
|
|
||||||
""" @INPUT 手动登录的账号(any) """
|
|
||||||
return cls(name=AliasAccount.INPUT.label, username=AliasAccount.INPUT.value, secret=None)
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def versions(self):
|
def versions(self):
|
||||||
return self.history.count()
|
return self.history.count()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_user_account(cls):
|
|
||||||
""" @USER 动态用户的账号(self) """
|
|
||||||
return cls(name=AliasAccount.USER.label, username=AliasAccount.USER.value, secret=None)
|
|
||||||
|
|
||||||
def get_su_from_accounts(self):
|
def get_su_from_accounts(self):
|
||||||
""" 排除自己和以自己为 su-from 的账号 """
|
""" 排除自己和以自己为 su-from 的账号 """
|
||||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||||
|
|
||||||
|
|
||||||
class AccountTemplate(BaseAccount):
|
def replace_history_model_with_mixin():
|
||||||
su_from = models.ForeignKey(
|
"""
|
||||||
'self', related_name='su_to', null=True,
|
替换历史模型中的父类为指定的Mixin类。
|
||||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
Parameters:
|
||||||
verbose_name = _('Account template')
|
model (class): 历史模型类,例如 Account.history.model
|
||||||
unique_together = (
|
mixin_class (class): 要替换为的Mixin类
|
||||||
('name', 'org_id'),
|
|
||||||
)
|
|
||||||
permissions = [
|
|
||||||
('view_accounttemplatesecret', _('Can view asset account template secret')),
|
|
||||||
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
Returns:
|
||||||
def get_su_from_account_templates(cls, instance=None):
|
None
|
||||||
if not instance:
|
"""
|
||||||
return cls.objects.all()
|
model = Account.history.model
|
||||||
return cls.objects.exclude(Q(id=instance.id) | Q(su_from=instance))
|
model.__bases__ = (VaultModelMixin,) + model.__bases__
|
||||||
|
|
||||||
def get_su_from_account(self, asset):
|
|
||||||
su_from = self.su_from
|
|
||||||
if su_from and asset.platform.su_enabled:
|
|
||||||
account = asset.accounts.filter(
|
|
||||||
username=su_from.username,
|
|
||||||
secret_type=su_from.secret_type
|
|
||||||
).first()
|
|
||||||
return account
|
|
||||||
|
|
||||||
def __str__(self):
|
replace_history_model_with_mixin()
|
||||||
return self.username
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bulk_update_accounts(accounts, data):
|
|
||||||
history_model = Account.history.model
|
|
||||||
account_ids = accounts.values_list('id', flat=True)
|
|
||||||
history_accounts = history_model.objects.filter(id__in=account_ids)
|
|
||||||
account_id_count_map = {
|
|
||||||
str(i['id']): i['count']
|
|
||||||
for i in history_accounts.values('id').order_by('id')
|
|
||||||
.annotate(count=Count(1)).values('id', 'count')
|
|
||||||
}
|
|
||||||
|
|
||||||
for account in accounts:
|
|
||||||
account_id = str(account.id)
|
|
||||||
account.version = account_id_count_map.get(account_id) + 1
|
|
||||||
for k, v in data.items():
|
|
||||||
setattr(account, k, v)
|
|
||||||
Account.objects.bulk_update(accounts, ['version', 'secret'])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bulk_create_history_accounts(accounts, user_id):
|
|
||||||
history_model = Account.history.model
|
|
||||||
history_account_objs = []
|
|
||||||
for account in accounts:
|
|
||||||
history_account_objs.append(
|
|
||||||
history_model(
|
|
||||||
id=account.id,
|
|
||||||
version=account.version,
|
|
||||||
secret=account.secret,
|
|
||||||
secret_type=account.secret_type,
|
|
||||||
history_user_id=user_id,
|
|
||||||
history_date=timezone.now()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
history_model.objects.bulk_create(history_account_objs)
|
|
||||||
|
|
||||||
def bulk_sync_account_secret(self, accounts, user_id):
|
|
||||||
""" 批量同步账号密码 """
|
|
||||||
if not accounts:
|
|
||||||
return
|
|
||||||
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
|
||||||
self.bulk_create_history_accounts(accounts, user_id)
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import uuid
|
|||||||
from celery import current_task
|
from celery import current_task
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.const.choices import Trigger
|
from common.const.choices import Trigger
|
||||||
from common.db.encoder import ModelJSONFieldEncoder
|
from common.db.encoder import ModelJSONFieldEncoder
|
||||||
@@ -22,9 +22,13 @@ logger = get_logger(__file__)
|
|||||||
|
|
||||||
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||||
types = models.JSONField(default=list)
|
types = models.JSONField(default=list)
|
||||||
recipients = models.ManyToManyField(
|
recipients_part_one = models.ManyToManyField(
|
||||||
'users.User', related_name='recipient_escape_route_plans', blank=True,
|
'users.User', related_name='recipient_part_one_plans', blank=True,
|
||||||
verbose_name=_("Recipient")
|
verbose_name=_("Recipient part one")
|
||||||
|
)
|
||||||
|
recipients_part_two = models.ManyToManyField(
|
||||||
|
'users.User', related_name='recipient_part_two_plans', blank=True,
|
||||||
|
verbose_name=_("Recipient part two")
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -52,9 +56,13 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
'org_id': self.org_id,
|
'org_id': self.org_id,
|
||||||
'created_by': self.created_by,
|
'created_by': self.created_by,
|
||||||
'types': self.types,
|
'types': self.types,
|
||||||
'recipients': {
|
'recipients_part_one': {
|
||||||
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
str(user.id): (str(user), bool(user.secret_key))
|
||||||
for recipient in self.recipients.all()
|
for user in self.recipients_part_one.all()
|
||||||
|
},
|
||||||
|
'recipients_part_two': {
|
||||||
|
str(user.id): (str(user), bool(user.secret_key))
|
||||||
|
for user in self.recipients_part_two.all()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +76,7 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
hid = str(uuid.uuid4())
|
hid = str(uuid.uuid4())
|
||||||
execution = AccountBackupExecution.objects.create(
|
execution = AccountBackupExecution.objects.create(
|
||||||
id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger
|
id=hid, plan=self, snapshot=self.to_attr_json(), trigger=trigger
|
||||||
)
|
)
|
||||||
return execution.start()
|
return execution.start()
|
||||||
|
|
||||||
@@ -85,7 +93,7 @@ class AccountBackupExecution(OrgModelMixin):
|
|||||||
timedelta = models.FloatField(
|
timedelta = models.FloatField(
|
||||||
default=0.0, verbose_name=_('Time'), null=True
|
default=0.0, verbose_name=_('Time'), null=True
|
||||||
)
|
)
|
||||||
plan_snapshot = models.JSONField(
|
snapshot = models.JSONField(
|
||||||
encoder=ModelJSONFieldEncoder, default=dict,
|
encoder=ModelJSONFieldEncoder, default=dict,
|
||||||
blank=True, null=True, verbose_name=_('Account backup snapshot')
|
blank=True, null=True, verbose_name=_('Account backup snapshot')
|
||||||
)
|
)
|
||||||
@@ -108,16 +116,9 @@ class AccountBackupExecution(OrgModelMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def types(self):
|
def types(self):
|
||||||
types = self.plan_snapshot.get('types')
|
types = self.snapshot.get('types')
|
||||||
return types
|
return types
|
||||||
|
|
||||||
@property
|
|
||||||
def recipients(self):
|
|
||||||
recipients = self.plan_snapshot.get('recipients')
|
|
||||||
if not recipients:
|
|
||||||
return []
|
|
||||||
return recipients.values()
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def backup_accounts(self):
|
def backup_accounts(self):
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import (
|
||||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||||
@@ -86,7 +86,7 @@ class ChangeSecretRecord(JMSBaseModel):
|
|||||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
||||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
|
||||||
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
||||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
||||||
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
||||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
||||||
status = models.CharField(max_length=16, default='pending')
|
status = models.CharField(max_length=16, default='pending')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes, Source
|
from accounts.const import AutomationTypes, Source
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from .base import AccountBaseAutomation
|
from .base import AccountBaseAutomation
|
||||||
|
|||||||
@@ -6,36 +6,35 @@ from hashlib import md5
|
|||||||
import sshpubkeys
|
import sshpubkeys
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType
|
||||||
from common.db import fields
|
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||||
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
||||||
)
|
)
|
||||||
|
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
||||||
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountQuerySet(models.QuerySet):
|
class BaseAccountQuerySet(VaultQuerySetMixin, models.QuerySet):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.filter(is_active=True)
|
return self.filter(is_active=True)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountManager(OrgManager):
|
class BaseAccountManager(VaultManagerMixin, OrgManager):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.get_queryset().active()
|
return self.get_queryset().active()
|
||||||
|
|
||||||
|
|
||||||
class BaseAccount(JMSOrgBaseModel):
|
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||||
secret_type = models.CharField(
|
secret_type = models.CharField(
|
||||||
max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||||
)
|
)
|
||||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
|
||||||
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||||
|
|
||||||
|
|||||||
1
apps/accounts/models/mixins/__init__.py
Normal file
1
apps/accounts/models/mixins/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .vault import *
|
||||||
94
apps/accounts/models/mixins/vault.py
Normal file
94
apps/accounts/models/mixins/vault.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.db import fields
|
||||||
|
|
||||||
|
__all__ = ['VaultQuerySetMixin', 'VaultManagerMixin', 'VaultModelMixin']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultQuerySetMixin(models.QuerySet):
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
"""
|
||||||
|
1. 替换 secret 为 _secret
|
||||||
|
2. 触发 post_save 信号
|
||||||
|
"""
|
||||||
|
if 'secret' in kwargs:
|
||||||
|
kwargs.update({
|
||||||
|
'_secret': kwargs.pop('secret')
|
||||||
|
})
|
||||||
|
rows = super().update(**kwargs)
|
||||||
|
|
||||||
|
# 为了获取更新后的对象所以单独查询一次
|
||||||
|
ids = self.values_list('id', flat=True)
|
||||||
|
objs = self.model.objects.filter(id__in=ids)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=False)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class VaultManagerMixin(models.Manager):
|
||||||
|
""" 触发 bulk_create 和 bulk_update 操作下的 post_save 信号 """
|
||||||
|
|
||||||
|
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
objs = super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=True)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=False)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
|
class VaultModelMixin(models.Model):
|
||||||
|
_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||||
|
is_sync_metadata = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
# 缓存 secret 值, lazy-property 不能用
|
||||||
|
__secret = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret(self):
|
||||||
|
if self.__secret:
|
||||||
|
return self.__secret
|
||||||
|
from accounts.backends import vault_client
|
||||||
|
secret = vault_client.get(self)
|
||||||
|
if not secret and not self.secret_has_save_to_vault:
|
||||||
|
# vault_client 获取不到, 并且 secret 没有保存到 vault, 就从 self._secret 获取
|
||||||
|
secret = self._secret
|
||||||
|
self.__secret = secret
|
||||||
|
return self.__secret
|
||||||
|
|
||||||
|
@secret.setter
|
||||||
|
def secret(self, value):
|
||||||
|
"""
|
||||||
|
保存的时候通过 post_save 信号监听进行处理,
|
||||||
|
先保存到 db, 再保存到 vault 同时删除本地 db _secret 值
|
||||||
|
"""
|
||||||
|
self._secret = value
|
||||||
|
self.__secret = value
|
||||||
|
|
||||||
|
_secret_save_to_vault_mark = '# Secret-has-been-saved-to-vault #'
|
||||||
|
|
||||||
|
def mark_secret_save_to_vault(self):
|
||||||
|
self._secret = self._secret_save_to_vault_mark
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret_has_save_to_vault(self):
|
||||||
|
return self._secret == self._secret_save_to_vault_mark
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
""" 通过 post_save signal 处理 _secret 数据 """
|
||||||
|
update_fields = kwargs.get('update_fields')
|
||||||
|
if update_fields and 'secret' in update_fields:
|
||||||
|
update_fields.remove('secret')
|
||||||
|
update_fields.append('_secret')
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
86
apps/accounts/models/template.py
Normal file
86
apps/accounts/models/template.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
|
from .base import BaseAccount
|
||||||
|
|
||||||
|
__all__ = ['AccountTemplate', ]
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTemplate(BaseAccount):
|
||||||
|
su_from = models.ForeignKey(
|
||||||
|
'self', related_name='su_to', null=True,
|
||||||
|
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Account template')
|
||||||
|
unique_together = (
|
||||||
|
('name', 'org_id'),
|
||||||
|
)
|
||||||
|
permissions = [
|
||||||
|
('view_accounttemplatesecret', _('Can view asset account template secret')),
|
||||||
|
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_su_from_account_templates(cls, pk=None):
|
||||||
|
if pk is None:
|
||||||
|
return cls.objects.all()
|
||||||
|
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name}({self.username})'
|
||||||
|
|
||||||
|
def get_su_from_account(self, asset):
|
||||||
|
su_from = self.su_from
|
||||||
|
if su_from and asset.platform.su_enabled:
|
||||||
|
account = asset.accounts.filter(
|
||||||
|
username=su_from.username,
|
||||||
|
secret_type=su_from.secret_type
|
||||||
|
).first()
|
||||||
|
return account
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bulk_update_accounts(accounts, data):
|
||||||
|
history_model = Account.history.model
|
||||||
|
account_ids = accounts.values_list('id', flat=True)
|
||||||
|
history_accounts = history_model.objects.filter(id__in=account_ids)
|
||||||
|
account_id_count_map = {
|
||||||
|
str(i['id']): i['count']
|
||||||
|
for i in history_accounts.values('id').order_by('id')
|
||||||
|
.annotate(count=Count(1)).values('id', 'count')
|
||||||
|
}
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
account_id = str(account.id)
|
||||||
|
account.version = account_id_count_map.get(account_id) + 1
|
||||||
|
for k, v in data.items():
|
||||||
|
setattr(account, k, v)
|
||||||
|
Account.objects.bulk_update(accounts, ['version', 'secret'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bulk_create_history_accounts(accounts, user_id):
|
||||||
|
history_model = Account.history.model
|
||||||
|
history_account_objs = []
|
||||||
|
for account in accounts:
|
||||||
|
history_account_objs.append(
|
||||||
|
history_model(
|
||||||
|
id=account.id,
|
||||||
|
version=account.version,
|
||||||
|
secret=account.secret,
|
||||||
|
secret_type=account.secret_type,
|
||||||
|
history_user_id=user_id,
|
||||||
|
history_date=timezone.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
history_model.objects.bulk_create(history_account_objs)
|
||||||
|
|
||||||
|
def bulk_sync_account_secret(self, accounts, user_id):
|
||||||
|
""" 批量同步账号密码 """
|
||||||
|
if not accounts:
|
||||||
|
return
|
||||||
|
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
||||||
|
self.bulk_create_history_accounts(accounts, user_id)
|
||||||
103
apps/accounts/models/virtual.py
Normal file
103
apps/accounts/models/virtual.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.const import AliasAccount
|
||||||
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
|
||||||
|
__all__ = ['VirtualAccount']
|
||||||
|
|
||||||
|
from orgs.utils import tmp_to_org
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualAccount(JMSOrgBaseModel):
|
||||||
|
alias = models.CharField(max_length=128, choices=AliasAccount.virtual_choices(), verbose_name=_('Alias'), )
|
||||||
|
secret_from_login = models.BooleanField(default=None, null=True, verbose_name=_("Secret from login"), )
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [('alias', 'org_id')]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.get_alias_display()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
usernames_map = {
|
||||||
|
AliasAccount.INPUT: _("Manual input"),
|
||||||
|
AliasAccount.USER: _("Same with user"),
|
||||||
|
AliasAccount.ANON: ''
|
||||||
|
}
|
||||||
|
usernames_map = {str(k): v for k, v in usernames_map.items()}
|
||||||
|
return usernames_map.get(self.alias, '')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comment(self):
|
||||||
|
comments_map = {
|
||||||
|
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
|
||||||
|
AliasAccount.USER: _('The account username name same with user on connect'),
|
||||||
|
AliasAccount.ANON: _('Connect asset without using a username and password, '
|
||||||
|
'and it only supports web-based and custom-type assets'),
|
||||||
|
}
|
||||||
|
comments_map = {str(k): v for k, v in comments_map.items()}
|
||||||
|
return comments_map.get(self.alias, '')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_init_queryset(cls):
|
||||||
|
aliases = [i[0] for i in AliasAccount.virtual_choices()]
|
||||||
|
alias_created = cls.objects.all().values_list('alias', flat=True)
|
||||||
|
need_created = set(aliases) - set(alias_created)
|
||||||
|
|
||||||
|
if need_created:
|
||||||
|
accounts = [cls(alias=alias) for alias in need_created]
|
||||||
|
cls.objects.bulk_create(accounts, ignore_conflicts=True)
|
||||||
|
return cls.objects.all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_special_account(cls, alias, user, asset, input_username='', input_secret='', from_permed=True):
|
||||||
|
if alias == AliasAccount.INPUT.value:
|
||||||
|
account = cls.get_manual_account(input_username, input_secret, from_permed)
|
||||||
|
elif alias == AliasAccount.ANON.value:
|
||||||
|
account = cls.get_anonymous_account()
|
||||||
|
elif alias == AliasAccount.USER.value:
|
||||||
|
account = cls.get_same_account(user, asset, input_secret=input_secret, from_permed=from_permed)
|
||||||
|
else:
|
||||||
|
account = cls(name=alias, username=alias, secret=None)
|
||||||
|
account.alias = alias
|
||||||
|
if asset:
|
||||||
|
account.asset = asset
|
||||||
|
account.org_id = asset.org_id
|
||||||
|
return account
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_manual_account(cls, input_username='', input_secret='', from_permed=True):
|
||||||
|
""" @INPUT 手动登录的账号(any) """
|
||||||
|
from .account import Account
|
||||||
|
if from_permed:
|
||||||
|
username = AliasAccount.INPUT.value
|
||||||
|
secret = ''
|
||||||
|
else:
|
||||||
|
username = input_username
|
||||||
|
secret = input_secret
|
||||||
|
return Account(name=AliasAccount.INPUT.label, username=username, secret=secret)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_anonymous_account(cls):
|
||||||
|
from .account import Account
|
||||||
|
return Account(name=AliasAccount.ANON.label, username=AliasAccount.ANON.value, secret=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_same_account(cls, user, asset, input_secret='', from_permed=True):
|
||||||
|
""" @USER 动态用户的账号(self) """
|
||||||
|
from .account import Account
|
||||||
|
username = user.username
|
||||||
|
|
||||||
|
with tmp_to_org(asset.org):
|
||||||
|
same_account = cls.objects.filter(alias='@USER').first()
|
||||||
|
|
||||||
|
secret = ''
|
||||||
|
if same_account and same_account.secret_from_login:
|
||||||
|
secret = user.get_cached_password_if_has()
|
||||||
|
|
||||||
|
if not secret and not from_permed:
|
||||||
|
secret = input_secret
|
||||||
|
return Account(name=AliasAccount.USER.label, username=username, secret=secret)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.tasks import send_mail_attachment_async
|
from common.tasks import send_mail_attachment_async
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .backup import *
|
from .backup import *
|
||||||
from .base import *
|
from .base import *
|
||||||
from .template import *
|
|
||||||
from .gathered_account import *
|
from .gathered_account import *
|
||||||
|
from .template import *
|
||||||
|
from .virtual import *
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
@@ -95,6 +95,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
|||||||
field.name for field in template._meta.fields
|
field.name for field in template._meta.fields
|
||||||
if field.name not in ignore_fields
|
if field.name not in ignore_fields
|
||||||
]
|
]
|
||||||
|
field_names = [name if name != '_secret' else 'secret' for name in field_names]
|
||||||
|
|
||||||
attrs = {}
|
attrs = {}
|
||||||
for name in field_names:
|
for name in field_names:
|
||||||
value = getattr(template, name, None)
|
value = getattr(template, name, None)
|
||||||
@@ -198,7 +200,6 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||||
asset = AccountAssetSerializer(label=_('Asset'))
|
asset = AccountAssetSerializer(label=_('Asset'))
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
|
||||||
source = LabeledChoiceField(
|
source = LabeledChoiceField(
|
||||||
choices=Source.choices, label=_("Source"), required=False,
|
choices=Source.choices, label=_("Source"), required=False,
|
||||||
allow_null=True, default=Source.LOCAL
|
allow_null=True, default=Source.LOCAL
|
||||||
@@ -233,6 +234,15 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDetailSerializer(AccountSerializer):
|
||||||
|
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||||
|
|
||||||
|
class Meta(AccountSerializer.Meta):
|
||||||
|
model = Account
|
||||||
|
fields = AccountSerializer.Meta.fields + ['has_secret']
|
||||||
|
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']
|
||||||
|
|
||||||
|
|
||||||
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||||
state = serializers.CharField(read_only=True, label=_('State'))
|
state = serializers.CharField(read_only=True, label=_('State'))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
||||||
@@ -24,7 +24,7 @@ class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSer
|
|||||||
]
|
]
|
||||||
fields = read_only_fields + [
|
fields = read_only_fields + [
|
||||||
'id', 'name', 'is_periodic', 'interval', 'crontab',
|
'id', 'name', 'is_periodic', 'interval', 'crontab',
|
||||||
'comment', 'recipients', 'types'
|
'comment', 'types', 'recipients_part_one', 'recipients_part_two'
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'required': True},
|
'name': {'required': True},
|
||||||
@@ -44,7 +44,7 @@ class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = AccountBackupExecution
|
model = AccountBackupExecution
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'id', 'date_start', 'timedelta', 'plan_snapshot',
|
'id', 'date_start', 'timedelta', 'snapshot',
|
||||||
'trigger', 'reason', 'is_success', 'org_id', 'recipients'
|
'trigger', 'reason', 'is_success', 'org_id'
|
||||||
]
|
]
|
||||||
fields = read_only_fields + ['plan']
|
fields = read_only_fields + ['plan']
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType
|
||||||
@@ -61,22 +61,23 @@ class AuthValidateMixin(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BaseAccount
|
model = BaseAccount
|
||||||
fields_mini = ['id', 'name', 'username']
|
fields_mini = ['id', 'name', 'username']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'secret_type', 'secret', 'has_secret', 'passphrase',
|
'secret_type', 'secret', 'passphrase',
|
||||||
'privileged', 'is_active', 'spec_info',
|
'privileged', 'is_active', 'spec_info',
|
||||||
]
|
]
|
||||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||||
fields = fields_small + fields_other
|
fields = fields_small + fields_other
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'has_secret', 'spec_info',
|
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||||
'date_verified', 'created_by', 'date_created',
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'spec_info': {'label': _('Spec info')},
|
'spec_info': {'label': _('Spec info')},
|
||||||
'username': {'help_text': _("Tip: If no username is required for authentication, fill in `null`")}
|
'username': {'help_text': _(
|
||||||
|
"Tip: If no username is required for authentication, fill in `null`, "
|
||||||
|
"If AD account, like `username@domain`"
|
||||||
|
)},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.models import GatheredAccount
|
from accounts.models import GatheredAccount
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import AccountTemplate, Account
|
from accounts.models import AccountTemplate, Account
|
||||||
|
|||||||
26
apps/accounts/serializers/account/virtual.py
Normal file
26
apps/accounts/serializers/account/virtual.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from accounts.models import VirtualAccount
|
||||||
|
|
||||||
|
__all__ = ['VirtualAccountSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualAccountSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = VirtualAccount
|
||||||
|
field_mini = ['id', 'alias', 'username', 'name']
|
||||||
|
common_fields = ['date_created', 'date_updated', 'comment']
|
||||||
|
fields = field_mini + [
|
||||||
|
'secret_from_login',
|
||||||
|
] + common_fields
|
||||||
|
read_only_fields = common_fields + common_fields
|
||||||
|
extra_kwargs = {
|
||||||
|
'comment': {'label': _('Comment')},
|
||||||
|
'name': {'label': _('Name')},
|
||||||
|
'username': {'label': _('Username')},
|
||||||
|
'secret_from_login': {'help_text': _('Current only support login from AD/LDAP. Secret priority: '
|
||||||
|
'Same account in asset secret > Login secret > Manual input')
|
||||||
|
},
|
||||||
|
'alias': {'required': False},
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import AutomationExecution
|
from accounts.models import AutomationExecution
|
||||||
@@ -63,15 +63,17 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_snapshot(obj):
|
def get_snapshot(obj):
|
||||||
tp = obj.snapshot['type']
|
tp = obj.snapshot.get('type', '')
|
||||||
|
type_display = tp if not hasattr(AutomationTypes, tp) \
|
||||||
|
else getattr(AutomationTypes, tp).label
|
||||||
snapshot = {
|
snapshot = {
|
||||||
'type': tp,
|
'type': tp,
|
||||||
'name': obj.snapshot['name'],
|
'name': obj.snapshot.get('name'),
|
||||||
'comment': obj.snapshot['comment'],
|
'comment': obj.snapshot.get('comment'),
|
||||||
'accounts': obj.snapshot['accounts'],
|
'accounts': obj.snapshot.get('accounts'),
|
||||||
'node_amount': len(obj.snapshot['nodes']),
|
'node_amount': len(obj.snapshot.get('nodes', [])),
|
||||||
'asset_amount': len(obj.snapshot['assets']),
|
'asset_amount': len(obj.snapshot.get('assets', [])),
|
||||||
'type_display': getattr(AutomationTypes, tp).label,
|
'type_display': type_display,
|
||||||
}
|
}
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import (
|
||||||
@@ -50,7 +50,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
|||||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||||
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
|
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
|
||||||
'secret_type', 'secret_strategy', 'secret', 'password_rules',
|
'secret_type', 'secret_strategy', 'secret', 'password_rules',
|
||||||
'ssh_key_change_strategy', 'passphrase', 'recipients',
|
'ssh_key_change_strategy', 'passphrase', 'recipients', 'params'
|
||||||
]
|
]
|
||||||
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
|
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
|
||||||
'accounts': {'required': True},
|
'accounts': {'required': True},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
|
|||||||
|
|
||||||
class Meta(ChangeSecretAutomationSerializer.Meta):
|
class Meta(ChangeSecretAutomationSerializer.Meta):
|
||||||
model = PushAccountAutomation
|
model = PushAccountAutomation
|
||||||
fields = ['params'] + [
|
fields = [
|
||||||
n for n in ChangeSecretAutomationSerializer.Meta.fields
|
n for n in ChangeSecretAutomationSerializer.Meta.fields
|
||||||
if n not in ['recipients']
|
if n not in ['recipients']
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save, post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from accounts.backends import vault_client
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .models import Account
|
from .models import Account, AccountTemplate
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -13,3 +14,23 @@ def on_account_pre_save(sender, instance, **kwargs):
|
|||||||
instance.version = 1
|
instance.version = 1
|
||||||
else:
|
else:
|
||||||
instance.version = instance.history.count()
|
instance.version = instance.history.count()
|
||||||
|
|
||||||
|
|
||||||
|
class VaultSignalHandler(object):
|
||||||
|
""" 处理 Vault 相关的信号 """
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_to_vault(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
vault_client.create(instance)
|
||||||
|
else:
|
||||||
|
vault_client.update(instance)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_to_vault(sender, instance, **kwargs):
|
||||||
|
vault_client.delete(instance)
|
||||||
|
|
||||||
|
|
||||||
|
for model in (Account, AccountTemplate, Account.history.model):
|
||||||
|
post_save.connect(VaultSignalHandler.save_to_vault, sender=model)
|
||||||
|
post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def task_activity_callback(self, pid, trigger, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
|
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
|
||||||
def execute_account_backup_task(pid, trigger):
|
def execute_account_backup_task(pid, trigger, **kwargs):
|
||||||
from accounts.models import AccountBackupAutomation
|
from accounts.models import AccountBackupAutomation
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
|
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils.translation import gettext_noop, ugettext_lazy as _
|
from django.utils.translation import gettext_noop, gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||||
|
|||||||
68
apps/accounts/tasks/vault.py
Normal file
68
apps/accounts/tasks/vault.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.backends import vault_client
|
||||||
|
from accounts.models import Account, AccountTemplate
|
||||||
|
from common.utils import get_logger
|
||||||
|
from orgs.utils import tmp_to_root_org
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_instance(instance):
|
||||||
|
instance_desc = f'[{instance._meta.verbose_name}-{instance.id}-{instance}]'
|
||||||
|
if instance.secret_has_save_to_vault:
|
||||||
|
msg = f'\033[32m- 跳过同步: {instance_desc}, 原因: [已同步]'
|
||||||
|
return "skipped", msg
|
||||||
|
|
||||||
|
try:
|
||||||
|
vault_client.create(instance)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f'\033[31m- 同步失败: {instance_desc}, 原因: [{e}]'
|
||||||
|
return "failed", msg
|
||||||
|
else:
|
||||||
|
msg = f'\033[32m- 同步成功: {instance_desc}'
|
||||||
|
return "succeeded", msg
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(verbose_name=_('Sync secret to vault'))
|
||||||
|
def sync_secret_to_vault():
|
||||||
|
if not vault_client.enabled:
|
||||||
|
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
|
||||||
|
print('\033[35m>>> 当前 Vault 功能未开启, 不需要同步')
|
||||||
|
return
|
||||||
|
|
||||||
|
failed, skipped, succeeded = 0, 0, 0
|
||||||
|
to_sync_models = [Account, AccountTemplate, Account.history.model]
|
||||||
|
print(f'\033[33m>>> 开始同步密钥数据到 Vault ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||||
|
with tmp_to_root_org():
|
||||||
|
instances = []
|
||||||
|
for model in to_sync_models:
|
||||||
|
instances += list(model.objects.all())
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
tasks = [executor.submit(sync_instance, instance) for instance in instances]
|
||||||
|
|
||||||
|
for future in as_completed(tasks):
|
||||||
|
status, msg = future.result()
|
||||||
|
print(msg)
|
||||||
|
if status == "succeeded":
|
||||||
|
succeeded += 1
|
||||||
|
elif status == "failed":
|
||||||
|
failed += 1
|
||||||
|
elif status == "skipped":
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
total = succeeded + failed + skipped
|
||||||
|
print(
|
||||||
|
f'\033[33m>>> 同步完成: {model.__module__}, '
|
||||||
|
f'共计: {total}, '
|
||||||
|
f'成功: {succeeded}, '
|
||||||
|
f'失败: {failed}, '
|
||||||
|
f'跳过: {skipped}'
|
||||||
|
)
|
||||||
|
print(f'\033[33m>>> 全部同步完成 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||||
|
print('\033[0m')
|
||||||
@@ -9,6 +9,7 @@ app_name = 'accounts'
|
|||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
|
|
||||||
router.register(r'accounts', api.AccountViewSet, 'account')
|
router.register(r'accounts', api.AccountViewSet, 'account')
|
||||||
|
router.register(r'virtual-accounts', api.VirtualAccountViewSet, 'virtual-account')
|
||||||
router.register(r'gathered-accounts', api.GatheredAccountViewSet, 'gathered-account')
|
router.register(r'gathered-accounts', api.GatheredAccountViewSet, 'gathered-account')
|
||||||
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
||||||
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
|
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
|
||||||
@@ -39,7 +40,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('push-account/<uuid:pk>/asset/remove/', api.PushAccountRemoveAssetApi.as_view(),
|
path('push-account/<uuid:pk>/asset/remove/', api.PushAccountRemoveAssetApi.as_view(),
|
||||||
name='push-account-remove-asset'),
|
name='push-account-remove-asset'),
|
||||||
path('push-accountt/<uuid:pk>/asset/add/', api.PushAccountAddAssetApi.as_view(), name='push-account-add-asset'),
|
path('push-account/<uuid:pk>/asset/add/', api.PushAccountAddAssetApi.as_view(), name='push-account-add-asset'),
|
||||||
path('push-account/<uuid:pk>/nodes/', api.PushAccountNodeAddRemoveApi.as_view(),
|
path('push-account/<uuid:pk>/nodes/', api.PushAccountNodeAddRemoveApi.as_view(),
|
||||||
name='push-account-add-or-remove-node'),
|
name='push-account-add-or-remove-node'),
|
||||||
path('push-account/<uuid:pk>/assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'),
|
path('push-account/<uuid:pk>/assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'),
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
||||||
SecretType, DEFAULT_PASSWORD_RULES
|
from common.utils import ssh_key_gen, random_string
|
||||||
)
|
|
||||||
from common.utils import gen_key_pair, random_string
|
|
||||||
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +14,7 @@ class SecretGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_ssh_key():
|
def generate_ssh_key():
|
||||||
private_key, public_key = gen_key_pair()
|
private_key, public_key = ssh_key_gen()
|
||||||
return private_key
|
return private_key
|
||||||
|
|
||||||
def generate_password(self):
|
def generate_password(self):
|
||||||
@@ -41,6 +39,8 @@ def validate_password_for_ansible(password):
|
|||||||
# Ansible 推送的时候不支持
|
# Ansible 推送的时候不支持
|
||||||
if '{{' in password:
|
if '{{' in password:
|
||||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||||
|
if '{%' in password:
|
||||||
|
raise serializers.ValidationError(_('Password can not contains `{%` '))
|
||||||
# Ansible Windows 推送的时候不支持
|
# Ansible Windows 推送的时候不支持
|
||||||
if "'" in password:
|
if "'" in password:
|
||||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AclsConfig(AppConfig):
|
class AclsConfig(AppConfig):
|
||||||
|
|||||||
9
apps/acls/const.py
Normal file
9
apps/acls/const.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ActionChoices(models.TextChoices):
|
||||||
|
reject = 'reject', _('Reject')
|
||||||
|
accept = 'accept', _('Accept')
|
||||||
|
review = 'review', _('Review')
|
||||||
|
warning = 'warning', _('Warning')
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
# Generated by Django 3.2.17 on 2023-06-06 10:57
|
# Generated by Django 3.2.17 on 2023-06-06 10:57
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -8,17 +7,20 @@ import common.db.fields
|
|||||||
|
|
||||||
def migrate_users_login_acls(apps, schema_editor):
|
def migrate_users_login_acls(apps, schema_editor):
|
||||||
login_acl_model = apps.get_model('acls', 'LoginACL')
|
login_acl_model = apps.get_model('acls', 'LoginACL')
|
||||||
name_used = defaultdict(int)
|
|
||||||
|
|
||||||
for login_acl in login_acl_model.objects.all():
|
name_used = []
|
||||||
name = login_acl.name
|
login_acls = []
|
||||||
if name_used[name] > 0:
|
for login_acl in login_acl_model.objects.all().select_related('user'):
|
||||||
login_acl.name += "_{}".format(name_used[name])
|
name = '{}_{}'.format(login_acl.name, login_acl.user.username)
|
||||||
name_used[name] += 1
|
if name.lower() in name_used:
|
||||||
|
name += '_{}'.format(str(login_acl.user_id)[:4])
|
||||||
|
name_used.append(name.lower())
|
||||||
|
login_acl.name = name
|
||||||
login_acl.users = {
|
login_acl.users = {
|
||||||
"type": "ids", "ids": [str(login_acl.user_id)]
|
"type": "ids", "ids": [str(login_acl.user_id)]
|
||||||
}
|
}
|
||||||
login_acl.save()
|
login_acls.append(login_acl)
|
||||||
|
login_acl_model.objects.bulk_update(login_acls, ['name', 'users'])
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from common.db.models import JMSBaseModel
|
|||||||
from common.utils import contains_ip
|
from common.utils import contains_ip
|
||||||
from common.utils.time_period import contains_time_period
|
from common.utils.time_period import contains_time_period
|
||||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||||
|
from ..const import ActionChoices
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'BaseACL', 'UserBaseACL', 'UserAssetAccountBaseACL',
|
'BaseACL', 'UserBaseACL', 'UserAssetAccountBaseACL',
|
||||||
@@ -16,12 +17,6 @@ from orgs.utils import tmp_to_root_org
|
|||||||
from orgs.utils import tmp_to_org
|
from orgs.utils import tmp_to_org
|
||||||
|
|
||||||
|
|
||||||
class ActionChoices(models.TextChoices):
|
|
||||||
reject = 'reject', _('Reject')
|
|
||||||
accept = 'accept', _('Accept')
|
|
||||||
review = 'review', _('Review')
|
|
||||||
|
|
||||||
|
|
||||||
class BaseACLQuerySet(models.QuerySet):
|
class BaseACLQuerySet(models.QuerySet):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.filter(is_active=True)
|
return self.filter(is_active=True)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import lazyproperty, get_logger
|
from common.utils import lazyproperty, get_logger
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_request_ip, get_ip_city
|
from common.utils import get_request_ip, get_ip_city
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .base import UserAssetAccountBaseACL
|
from .base import UserAssetAccountBaseACL
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from acls.models.base import ActionChoices, BaseACL
|
from acls.models.base import BaseACL
|
||||||
from common.serializers.fields import JSONManyToManyField, LabeledChoiceField
|
from common.serializers.fields import JSONManyToManyField, LabeledChoiceField
|
||||||
from jumpserver.utils import has_valid_xpack_license
|
from jumpserver.utils import has_valid_xpack_license
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
|
from ..const import ActionChoices
|
||||||
|
|
||||||
common_help_text = _(
|
common_help_text = _(
|
||||||
"With * indicating a match all. "
|
"With * indicating a match all. "
|
||||||
@@ -60,18 +61,21 @@ class ActionAclSerializer(serializers.Serializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.set_action_choices()
|
self.set_action_choices()
|
||||||
|
|
||||||
def set_action_choices(self):
|
|
||||||
action = self.fields.get("action")
|
|
||||||
if not action:
|
|
||||||
return
|
|
||||||
choices = action.choices
|
|
||||||
if not has_valid_xpack_license():
|
|
||||||
choices.pop(ActionChoices.review, None)
|
|
||||||
action._choices = choices
|
|
||||||
|
|
||||||
|
|
||||||
class BaserACLSerializer(ActionAclSerializer, serializers.Serializer):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
action_choices_exclude = [ActionChoices.warning]
|
||||||
|
|
||||||
|
def set_action_choices(self):
|
||||||
|
field_action = self.fields.get("action")
|
||||||
|
if not field_action:
|
||||||
|
return
|
||||||
|
if not has_valid_xpack_license():
|
||||||
|
field_action._choices.pop(ActionChoices.review, None)
|
||||||
|
for choice in self.Meta.action_choices_exclude:
|
||||||
|
field_action._choices.pop(choice, None)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||||
|
class Meta(ActionAclSerializer.Meta):
|
||||||
model = BaseACL
|
model = BaseACL
|
||||||
fields_mini = ["id", "name"]
|
fields_mini = ["id", "name"]
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
@@ -84,6 +88,7 @@ class BaserACLSerializer(ActionAclSerializer, serializers.Serializer):
|
|||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"priority": {"default": 50},
|
"priority": {"default": 50},
|
||||||
"is_active": {"default": True},
|
"is_active": {"default": True},
|
||||||
|
'reviewers': {'label': _('Recipients')},
|
||||||
}
|
}
|
||||||
|
|
||||||
def validate_reviewers(self, reviewers):
|
def validate_reviewers(self, reviewers):
|
||||||
@@ -107,16 +112,16 @@ class BaserACLSerializer(ActionAclSerializer, serializers.Serializer):
|
|||||||
return valid_reviewers
|
return valid_reviewers
|
||||||
|
|
||||||
|
|
||||||
class BaserUserACLSerializer(BaserACLSerializer):
|
class BaseUserACLSerializer(BaseACLSerializer):
|
||||||
users = JSONManyToManyField(label=_('User'))
|
users = JSONManyToManyField(label=_('User'))
|
||||||
|
|
||||||
class Meta(BaserACLSerializer.Meta):
|
class Meta(BaseACLSerializer.Meta):
|
||||||
fields = BaserACLSerializer.Meta.fields + ['users']
|
fields = BaseACLSerializer.Meta.fields + ['users']
|
||||||
|
|
||||||
|
|
||||||
class BaseUserAssetAccountACLSerializer(BaserUserACLSerializer):
|
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
|
||||||
assets = JSONManyToManyField(label=_('Asset'))
|
assets = JSONManyToManyField(label=_('Asset'))
|
||||||
accounts = serializers.ListField(label=_('Account'))
|
accounts = serializers.ListField(label=_('Account'))
|
||||||
|
|
||||||
class Meta(BaserUserACLSerializer.Meta):
|
class Meta(BaseUserACLSerializer.Meta):
|
||||||
fields = BaserUserACLSerializer.Meta.fields + ['assets', 'accounts']
|
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts']
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from acls.models import CommandGroup, CommandFilterACL
|
from acls.models import CommandGroup, CommandFilterACL
|
||||||
@@ -31,6 +31,8 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
|||||||
class Meta(BaseSerializer.Meta):
|
class Meta(BaseSerializer.Meta):
|
||||||
model = CommandFilterACL
|
model = CommandFilterACL
|
||||||
fields = BaseSerializer.Meta.fields + ['command_groups']
|
fields = BaseSerializer.Meta.fields + ['command_groups']
|
||||||
|
# 默认都支持所有的 actions
|
||||||
|
action_choices_exclude = []
|
||||||
|
|
||||||
|
|
||||||
class CommandReviewSerializer(serializers.Serializer):
|
class CommandReviewSerializer(serializers.Serializer):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||||
from ..models import ConnectMethodACL
|
from ..models import ConnectMethodACL
|
||||||
|
from ..const import ActionChoices
|
||||||
|
|
||||||
__all__ = ["ConnectMethodACLSerializer"]
|
__all__ = ["ConnectMethodACLSerializer"]
|
||||||
|
|
||||||
@@ -12,12 +13,6 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
|||||||
i for i in BaseSerializer.Meta.fields + ['connect_methods']
|
i for i in BaseSerializer.Meta.fields + ['connect_methods']
|
||||||
if i not in ['assets', 'accounts']
|
if i not in ['assets', 'accounts']
|
||||||
]
|
]
|
||||||
|
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
||||||
def __init__(self, *args, **kwargs):
|
ActionChoices.review, ActionChoices.accept
|
||||||
super().__init__(*args, **kwargs)
|
]
|
||||||
field_action = self.fields.get('action')
|
|
||||||
if not field_action:
|
|
||||||
return
|
|
||||||
# 仅支持拒绝
|
|
||||||
for k in ['review', 'accept']:
|
|
||||||
field_action._choices.pop(k, None)
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from common.serializers import MethodSerializer
|
from common.serializers import MethodSerializer
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from .base import BaserUserACLSerializer
|
from .base import BaseUserACLSerializer
|
||||||
from .rules import RuleSerializer
|
from .rules import RuleSerializer
|
||||||
from ..models import LoginACL
|
from ..models import LoginACL
|
||||||
|
|
||||||
@@ -11,12 +11,12 @@ __all__ = ["LoginACLSerializer"]
|
|||||||
common_help_text = _("With * indicating a match all. ")
|
common_help_text = _("With * indicating a match all. ")
|
||||||
|
|
||||||
|
|
||||||
class LoginACLSerializer(BaserUserACLSerializer, BulkOrgResourceModelSerializer):
|
class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
|
||||||
rules = MethodSerializer(label=_('Rule'))
|
rules = MethodSerializer(label=_('Rule'))
|
||||||
|
|
||||||
class Meta(BaserUserACLSerializer.Meta):
|
class Meta(BaseUserACLSerializer.Meta):
|
||||||
model = LoginACL
|
model = LoginACL
|
||||||
fields = BaserUserACLSerializer.Meta.fields + ['rules', ]
|
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
|
||||||
|
|
||||||
def get_rules_serializer(self):
|
def get_rules_serializer(self):
|
||||||
return RuleSerializer()
|
return RuleSerializer()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
#
|
#
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ApplicationsConfig(AppConfig):
|
class ApplicationsConfig(AppConfig):
|
||||||
@@ -9,5 +9,4 @@ class ApplicationsConfig(AppConfig):
|
|||||||
verbose_name = _('Applications')
|
verbose_name = _('Applications')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signal_handlers
|
|
||||||
super().ready()
|
super().ready()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django_mysql.models
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
@@ -127,7 +126,7 @@ class Migration(migrations.Migration):
|
|||||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||||
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
|
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
|
||||||
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
|
||||||
('attrs', django_mysql.models.JSONField(default=dict)),
|
('attrs', models.JSONField(default=dict)),
|
||||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||||
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
|
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from .cloud import *
|
|||||||
from .custom import *
|
from .custom import *
|
||||||
from .database import *
|
from .database import *
|
||||||
from .device import *
|
from .device import *
|
||||||
|
from .gpt import *
|
||||||
from .host import *
|
from .host import *
|
||||||
from .permission import *
|
from .permission import *
|
||||||
from .web import *
|
from .web import *
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class AssetFilterSet(BaseFilterSet):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_protocols(queryset, name, value):
|
def filter_protocols(queryset, name, value):
|
||||||
value = value.split(',')
|
value = value.split(',')
|
||||||
return queryset.filter(protocols__name__in=value)
|
return queryset.filter(protocols__name__in=value).distinct()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_labels(queryset, name, value):
|
def filter_labels(queryset, name, value):
|
||||||
@@ -91,7 +91,7 @@ class AssetFilterSet(BaseFilterSet):
|
|||||||
queryset = queryset.filter(labels__name=n, labels__value=v)
|
queryset = queryset.filter(labels__name=n, labels__value=v)
|
||||||
else:
|
else:
|
||||||
q = Q(labels__name__contains=value) | Q(labels__value__contains=value)
|
q = Q(labels__name__contains=value) | Q(labels__value__contains=value)
|
||||||
queryset = queryset.filter(q)
|
queryset = queryset.filter(q).distinct()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +121,14 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||||||
NodeFilterBackend, AttrRulesFilterBackend
|
NodeFilterBackend, AttrRulesFilterBackend
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset() \
|
||||||
|
.prefetch_related('nodes', 'protocols') \
|
||||||
|
.select_related('platform', 'domain')
|
||||||
|
if queryset.model is not Asset:
|
||||||
|
queryset = queryset.select_related('asset_ptr')
|
||||||
|
return queryset
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
cls = super().get_serializer_class()
|
cls = super().get_serializer_class()
|
||||||
if self.action == "retrieve":
|
if self.action == "retrieve":
|
||||||
|
|||||||
16
apps/assets/api/asset/gpt.py
Normal file
16
apps/assets/api/asset/gpt.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from assets.models import GPT, Asset
|
||||||
|
from assets.serializers import GPTSerializer
|
||||||
|
|
||||||
|
from .asset import AssetViewSet
|
||||||
|
|
||||||
|
__all__ = ['GPTViewSet']
|
||||||
|
|
||||||
|
|
||||||
|
class GPTViewSet(AssetViewSet):
|
||||||
|
model = GPT
|
||||||
|
perm_model = Asset
|
||||||
|
|
||||||
|
def get_serializer_classes(self):
|
||||||
|
serializer_classes = super().get_serializer_classes()
|
||||||
|
serializer_classes['default'] = GPTSerializer
|
||||||
|
return serializer_classes
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
from rest_framework.mixins import ListModelMixin
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.mixins import ListModelMixin
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from assets.const import AllTypes
|
||||||
|
from assets.serializers import CategorySerializer, TypeSerializer
|
||||||
from common.api import JMSGenericViewSet
|
from common.api import JMSGenericViewSet
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
from assets.serializers import CategorySerializer, TypeSerializer
|
|
||||||
from assets.const import AllTypes
|
|
||||||
|
|
||||||
__all__ = ['CategoryViewSet']
|
__all__ = ['CategoryViewSet']
|
||||||
|
|
||||||
@@ -32,4 +32,3 @@ class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
|
|||||||
tp = request.query_params.get('type')
|
tp = request.query_params.get('type')
|
||||||
constraints = AllTypes.get_constraints(category, tp)
|
constraints = AllTypes.get_constraints(category, tp)
|
||||||
return Response(constraints)
|
return Response(constraints)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
@@ -26,6 +26,9 @@ class DomainViewSet(OrgBulkModelViewSet):
|
|||||||
return serializers.DomainWithGatewaySerializer
|
return serializers.DomainWithGatewaySerializer
|
||||||
return serializers.DomainSerializer
|
return serializers.DomainSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().prefetch_related('assets')
|
||||||
|
|
||||||
|
|
||||||
class GatewayViewSet(HostViewSet):
|
class GatewayViewSet(HostViewSet):
|
||||||
perm_model = Gateway
|
perm_model = Gateway
|
||||||
|
|||||||
@@ -38,5 +38,6 @@ class LabelViewSet(OrgBulkModelViewSet):
|
|||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.queryset = Label.objects.annotate(asset_count=Count("assets"))
|
self.queryset = Label.objects.prefetch_related(
|
||||||
|
'assets').annotate(asset_count=Count("assets"))
|
||||||
return self.queryset
|
return self.queryset
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import List
|
|||||||
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from assets.models import Node, PlatformProtocol, Protocol
|
from assets.models import Node, Protocol
|
||||||
from assets.utils import get_node_from_request, is_query_node_all_assets
|
from assets.utils import get_node_from_request, is_query_node_all_assets
|
||||||
from common.utils import lazyproperty, timeit
|
from common.utils import lazyproperty, timeit
|
||||||
|
|
||||||
@@ -70,25 +70,18 @@ class SerializeToTreeNodeMixin:
|
|||||||
|
|
||||||
@timeit
|
@timeit
|
||||||
def serialize_assets(self, assets, node_key=None, pid=None):
|
def serialize_assets(self, assets, node_key=None, pid=None):
|
||||||
sftp_enabled_platform = PlatformProtocol.objects \
|
|
||||||
.filter(name='ssh', setting__sftp_enabled=True) \
|
|
||||||
.values_list('platform', flat=True) \
|
|
||||||
.distinct()
|
|
||||||
if node_key is None:
|
if node_key is None:
|
||||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||||
else:
|
else:
|
||||||
get_pid = lambda asset: node_key
|
get_pid = lambda asset: node_key
|
||||||
ssh_asset_ids = [
|
sftp_asset_ids = Protocol.objects.filter(name='sftp') \
|
||||||
str(i) for i in
|
.values_list('asset_id', flat=True)
|
||||||
Protocol.objects.filter(name='ssh').values_list('asset_id', flat=True)
|
sftp_asset_ids = list(sftp_asset_ids)
|
||||||
]
|
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
'id': str(asset.id),
|
'id': str(asset.id),
|
||||||
'name': asset.name,
|
'name': asset.name,
|
||||||
'title':
|
'title': f'{asset.address}\n{asset.comment}',
|
||||||
f'{asset.address}\n{asset.comment}'
|
|
||||||
if asset.comment else asset.address,
|
|
||||||
'pId': pid or get_pid(asset),
|
'pId': pid or get_pid(asset),
|
||||||
'isParent': False,
|
'isParent': False,
|
||||||
'open': False,
|
'open': False,
|
||||||
@@ -99,8 +92,7 @@ class SerializeToTreeNodeMixin:
|
|||||||
'data': {
|
'data': {
|
||||||
'platform_type': asset.platform.type,
|
'platform_type': asset.platform.type,
|
||||||
'org_name': asset.org_name,
|
'org_name': asset.org_name,
|
||||||
'sftp': (asset.platform_id in sftp_enabled_platform) \
|
'sftp': asset.id in sftp_asset_ids,
|
||||||
and (str(asset.id) in ssh_asset_ids),
|
|
||||||
'name': asset.name,
|
'name': asset.name,
|
||||||
'address': asset.address
|
'address': asset.address
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from collections import namedtuple, defaultdict
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ from rest_framework.decorators import action
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from assets.const import AllTypes
|
from assets.const import AllTypes
|
||||||
from assets.models import Platform, Node, Asset
|
from assets.models import Platform, Node, Asset, PlatformProtocol
|
||||||
from assets.serializers import PlatformSerializer
|
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer
|
||||||
from common.api import JMSModelViewSet
|
from common.api import JMSModelViewSet
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
from common.serializers import GroupedChoiceSerializer
|
from common.serializers import GroupedChoiceSerializer
|
||||||
|
|
||||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi']
|
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet']
|
||||||
|
|
||||||
|
|
||||||
class AssetPlatformViewSet(JMSModelViewSet):
|
class AssetPlatformViewSet(JMSModelViewSet):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': PlatformSerializer,
|
'default': PlatformSerializer,
|
||||||
'categories': GroupedChoiceSerializer
|
'categories': GroupedChoiceSerializer,
|
||||||
}
|
}
|
||||||
filterset_fields = ['name', 'category', 'type']
|
filterset_fields = ['name', 'category', 'type']
|
||||||
search_fields = ['name']
|
search_fields = ['name']
|
||||||
@@ -25,7 +25,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
|||||||
'categories': 'assets.view_platform',
|
'categories': 'assets.view_platform',
|
||||||
'type_constraints': 'assets.view_platform',
|
'type_constraints': 'assets.view_platform',
|
||||||
'ops_methods': 'assets.view_platform',
|
'ops_methods': 'assets.view_platform',
|
||||||
'filter_nodes_assets': 'assets.view_platform'
|
'filter_nodes_assets': 'assets.view_platform',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -61,6 +61,15 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
|||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformProtocolViewSet(JMSModelViewSet):
|
||||||
|
queryset = PlatformProtocol.objects.all()
|
||||||
|
serializer_class = PlatformProtocolSerializer
|
||||||
|
filterset_fields = ['name', 'platform__name']
|
||||||
|
rbac_perms = {
|
||||||
|
'*': 'assets.add_platform'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlatformAutomationMethodsApi(generics.ListAPIView):
|
class PlatformAutomationMethodsApi(generics.ListAPIView):
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user