mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
468 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb11cb1774 | ||
|
|
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
|
||||
logs/*
|
||||
data/*
|
||||
.github
|
||||
tmp/*
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/----.md
vendored
3
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -6,8 +6,7 @@ labels: 类型:需求
|
||||
assignees:
|
||||
- ibuler
|
||||
- baijiangjie
|
||||
|
||||
|
||||
- wojiushixiaobai
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
|
||||
31
.github/workflows/issue-comment.yml
vendored
31
.github/workflows/issue-comment.yml
vendored
@@ -21,17 +21,44 @@ jobs:
|
||||
actions: 'remove-labels'
|
||||
labels: '状态:待反馈'
|
||||
|
||||
add-label-if-not-author:
|
||||
add-label-if-is-member:
|
||||
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:
|
||||
- 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
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '状态:待反馈'
|
||||
|
||||
- name: Remove require handle label
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,7 +35,6 @@ celerybeat-schedule.db
|
||||
docs/_build/
|
||||
xpack
|
||||
xpack.bak
|
||||
logs/*
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
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 VERSION
|
||||
@@ -8,9 +8,8 @@ WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.9-slim-buster
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
@@ -22,8 +21,10 @@ ARG DEPENDENCIES=" \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
@@ -36,13 +37,11 @@ ARG TOOLS=" \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
locales \
|
||||
nmap \
|
||||
openssh-client \
|
||||
procps \
|
||||
sshpass \
|
||||
telnet \
|
||||
unzip \
|
||||
vim \
|
||||
git \
|
||||
wget"
|
||||
|
||||
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 \
|
||||
&& 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
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& rm -rf /tmp/build
|
||||
|
||||
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/logs
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
ARG VERSION
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||
FROM jumpserver/core:${VERSION}
|
||||
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
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 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||
</p>
|
||||
|
||||
| :warning: 注意 :warning: |
|
||||
|:-------------------------------------------------------------------------------------------------------------------------:|
|
||||
| 3.0 架构上和 2.0 变化较大,建议全新安装一套环境来体验。如需升级,请务必升级前进行备份,并[查阅文档](https://kb.fit2cloud.com/?p=06638d69-f109-4333-b5bf-65b17b297ed9) |
|
||||
------------------------------
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
|
||||
--------------------------
|
||||
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||
|
||||
- **SSH**: Linux / Unix / 网络设备 等;
|
||||
- **Windows**: Web 方式连接 / 原生 RDP 连接;
|
||||
- **数据库**: MySQL / Oracle / SQLServer / PostgreSQL 等;
|
||||
- **Kubernetes**: 支持连接到 K8s 集群中的 Pods;
|
||||
- **数据库**: MySQL / MariaDB / PostgreSQL / Oracle / SQLServer / ClickHouse 等;
|
||||
- **NoSQL**: Redis / MongoDB 等;
|
||||
- **GPT**: ChatGPT 等;
|
||||
- **云服务**: Kubernetes / VMware vSphere 等;
|
||||
- **Web 站点**: 各类系统的 Web 管理后台;
|
||||
- **应用**: 通过 Remote App 连接各类应用。
|
||||
|
||||
@@ -81,29 +80,28 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运
|
||||
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
|
||||
|
||||
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 及微信交流群当中进行交流沟通。
|
||||
|
||||
**微信交流群**
|
||||
|
||||
<img src="https://download.jumpserver.org/images/wecom-group.jpeg" alt="微信群二维码" width="200"/>
|
||||
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
|
||||
|
||||
### 参与贡献
|
||||
|
||||
欢迎提交 PR 参与贡献。感谢以下贡献者,他们让 JumpServer 变的越来越好。
|
||||
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
||||
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
|
||||
|
||||
## 组件项目
|
||||
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目 |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer RDP 代理 Connector 项目 |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 |
|
||||
| [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
|
||||
- 电话:400-052-0755
|
||||
|
||||
## 致谢开源
|
||||
|
||||
- [Apache Guacamole](https://guacamole.apache.org/): Web 页面连接 RDP、SSH、VNC 等协议资产,JumpServer Lion 组件使用到该项目;
|
||||
- [OmniDB](https://omnidb.org/): Web 页面连接使用数据库,JumpServer Web 数据库组件使用到该项目。
|
||||
|
||||
## License & Copyright
|
||||
|
||||
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .account import *
|
||||
from .task import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
|
||||
@@ -22,10 +22,11 @@ __all__ = [
|
||||
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = Account
|
||||
search_fields = ('username', 'name', 'asset__name', 'asset__address')
|
||||
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
'retrieve': serializers.AccountDetailSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'partial_update': ['accounts.change_account'],
|
||||
@@ -52,20 +53,21 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(
|
||||
methods=['get'], detail=False, url_path='username-suggestions',
|
||||
methods=['post'], detail=False, url_path='username-suggestions',
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
asset_ids = request.query_params.get('assets')
|
||||
node_keys = request.query_params.get('keys')
|
||||
username = request.query_params.get('username')
|
||||
asset_ids = request.data.get('assets')
|
||||
node_ids = request.data.get('nodes')
|
||||
username = request.data.get('username')
|
||||
|
||||
assets = Asset.objects.all()
|
||||
if asset_ids:
|
||||
assets = assets.filter(id__in=asset_ids.split(','))
|
||||
if node_keys:
|
||||
patten = Node.get_node_all_children_key_pattern(node_keys.split(','))
|
||||
assets = assets.filter(nodes__key__regex=patten)
|
||||
assets = assets.filter(id__in=asset_ids)
|
||||
if node_ids:
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
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)
|
||||
if username:
|
||||
@@ -132,11 +134,13 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
||||
def get_queryset(self):
|
||||
account = self.get_object()
|
||||
histories = account.history.all()
|
||||
last_history = account.history.first()
|
||||
if not last_history:
|
||||
latest_history = account.history.first()
|
||||
if not latest_history:
|
||||
return histories
|
||||
|
||||
if account.secret == last_history.secret \
|
||||
and account.secret_type == last_history.secret_type:
|
||||
histories = histories.exclude(history_id=last_history.history_id)
|
||||
if account.secret != latest_history.secret:
|
||||
return histories
|
||||
if account.secret_type != latest_history.secret_type:
|
||||
return histories
|
||||
histories = histories.exclude(history_id=latest_history.history_id)
|
||||
return histories
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||
@action(methods=['get'], detail=False, url_path='su-from-account-templates')
|
||||
def su_from_account_templates(self, request, *args, **kwargs):
|
||||
pk = request.query_params.get('template_id')
|
||||
template = AccountTemplate.objects.filter(pk=pk).first()
|
||||
templates = AccountTemplate.get_su_from_account_templates(template)
|
||||
templates = AccountTemplate.get_su_from_account_templates(pk)
|
||||
templates = self.filter_queryset(templates)
|
||||
serializer = self.get_serializer(templates, many=True)
|
||||
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):
|
||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||
search_fields = ('trigger',)
|
||||
filterset_fields = ('trigger', 'plan_id')
|
||||
search_fields = ('trigger', 'plan__name')
|
||||
filterset_fields = ('trigger', 'plan_id', 'plan__name')
|
||||
http_method_names = ['get', 'post', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.response import Response
|
||||
|
||||
@@ -95,8 +95,8 @@ class AutomationExecutionViewSet(
|
||||
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
search_fields = ('trigger',)
|
||||
filterset_fields = ('trigger', 'automation_id')
|
||||
search_fields = ('trigger', 'automation__name')
|
||||
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||
serializer_class = serializers.AutomationExecutionSerializer
|
||||
|
||||
tp: str
|
||||
|
||||
@@ -6,6 +6,5 @@ class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
||||
|
||||
def ready(self):
|
||||
from . import signal_handlers
|
||||
from . import tasks
|
||||
__all__ = signal_handlers
|
||||
from . import signal_handlers # noqa
|
||||
from . import tasks # noqa
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import os
|
||||
import time
|
||||
from openpyxl import Workbook
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from openpyxl import Workbook
|
||||
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 users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
from accounts.serializers import AccountSecretSerializer
|
||||
from assets.const import AllTypes
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
|
||||
logger = get_logger(__file__)
|
||||
from common.utils.timezone import local_now_display
|
||||
from users.models import User
|
||||
|
||||
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||
|
||||
@@ -76,8 +71,22 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
)
|
||||
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
|
||||
def create_data_map(cls, accounts):
|
||||
def create_data_map(cls, accounts, section):
|
||||
data_map = defaultdict(list)
|
||||
|
||||
if not accounts.exists():
|
||||
@@ -97,9 +106,10 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
for tp, _accounts in account_type_map.items():
|
||||
sheet_name = type_dict.get(tp, tp)
|
||||
data = AccountSecretSerializer(_accounts, many=True).data
|
||||
cls.handler_secret(data, section)
|
||||
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
|
||||
|
||||
|
||||
@@ -109,8 +119,8 @@ class AccountBackupHandler:
|
||||
self.plan_name = self.execution.plan.name
|
||||
self.is_frozen = False # 任务状态冻结标志
|
||||
|
||||
def create_excel(self):
|
||||
logger.info(
|
||||
def create_excel(self, section='complete'):
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
||||
''
|
||||
@@ -119,7 +129,7 @@ class AccountBackupHandler:
|
||||
time_start = time.time()
|
||||
files = []
|
||||
accounts = self.execution.backup_accounts
|
||||
data_map = AssetAccountHandler.create_data_map(accounts)
|
||||
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
||||
if not data_map:
|
||||
return files
|
||||
|
||||
@@ -133,14 +143,14 @@ class AccountBackupHandler:
|
||||
wb.save(filename)
|
||||
files.append(filename)
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
logger.info('步骤完成: 用时 {}s'.format(timedelta))
|
||||
print('步骤完成: 用时 {}s'.format(timedelta))
|
||||
return files
|
||||
|
||||
def send_backup_mail(self, files, recipients):
|
||||
if not files:
|
||||
return
|
||||
recipients = User.objects.filter(id__in=list(recipients))
|
||||
logger.info(
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 发送备份邮件\033[0m'
|
||||
''
|
||||
@@ -155,7 +165,7 @@ class AccountBackupHandler:
|
||||
encrypt_and_compress_zip_file(attachment, password, files)
|
||||
attachment_list = [attachment, ]
|
||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
||||
logger.info('邮件已发送至{}({})'.format(user, user.email))
|
||||
print('邮件已发送至{}({})'.format(user, user.email))
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
@@ -163,33 +173,42 @@ class AccountBackupHandler:
|
||||
self.execution.reason = reason[:1024]
|
||||
self.execution.is_success = is_success
|
||||
self.execution.save()
|
||||
logger.info('已完成对任务状态的更新')
|
||||
print('已完成对任务状态的更新')
|
||||
|
||||
def step_finished(self, is_success):
|
||||
@staticmethod
|
||||
def step_finished(is_success):
|
||||
if is_success:
|
||||
logger.info('任务执行成功')
|
||||
print('任务执行成功')
|
||||
else:
|
||||
logger.error('任务执行失败')
|
||||
print('任务执行失败')
|
||||
|
||||
def _run(self):
|
||||
is_success = False
|
||||
error = '-'
|
||||
try:
|
||||
recipients = self.execution.plan_snapshot.get('recipients')
|
||||
if not recipients:
|
||||
logger.info(
|
||||
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||
if not recipients_part_one and not recipients_part_two:
|
||||
print(
|
||||
'\n'
|
||||
'\033[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:
|
||||
recipients = recipients_part_one or recipients_part_two
|
||||
files = self.create_excel()
|
||||
self.send_backup_mail(files, recipients)
|
||||
except Exception as e:
|
||||
self.is_frozen = True
|
||||
logger.error('任务执行被异常中断')
|
||||
logger.info('下面打印发生异常的 Traceback 信息 : ')
|
||||
logger.error(e, exc_info=True)
|
||||
print('任务执行被异常中断')
|
||||
print('下面打印发生异常的 Traceback 信息 : ')
|
||||
print(e)
|
||||
error = str(e)
|
||||
else:
|
||||
is_success = True
|
||||
@@ -199,15 +218,15 @@ class AccountBackupHandler:
|
||||
self.step_finished(is_success)
|
||||
|
||||
def run(self):
|
||||
logger.info('任务开始: {}'.format(local_now_display()))
|
||||
print('任务开始: {}'.format(local_now_display()))
|
||||
time_start = time.time()
|
||||
try:
|
||||
self._run()
|
||||
except Exception as e:
|
||||
logger.error('任务运行出现异常')
|
||||
logger.error('下面显示异常 Traceback 信息: ')
|
||||
logger.error(e, exc_info=True)
|
||||
print('任务运行出现异常')
|
||||
print('下面显示异常 Traceback 信息: ')
|
||||
print(e)
|
||||
finally:
|
||||
logger.info('\n任务结束: {}'.format(local_now_display()))
|
||||
print('\n任务结束: {}'.format(local_now_display()))
|
||||
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 common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
|
||||
from .handlers import AccountBackupHandler
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AccountBackupManager:
|
||||
def __init__(self, execution):
|
||||
@@ -23,7 +19,7 @@ class AccountBackupManager:
|
||||
|
||||
def do_run(self):
|
||||
execution = self.execution
|
||||
logger.info('\n\033[33m# 账号备份计划正在执行\033[0m')
|
||||
print('\n\033[33m# 账号备份计划正在执行\033[0m')
|
||||
handler = AccountBackupHandler(execution)
|
||||
handler.run()
|
||||
|
||||
@@ -35,10 +31,10 @@ class AccountBackupManager:
|
||||
self.time_end = time.time()
|
||||
self.date_end = timezone.now()
|
||||
|
||||
logger.info('\n\n' + '-' * 80)
|
||||
logger.info('计划执行结束 {}\n'.format(local_now_display()))
|
||||
print('\n\n' + '-' * 80)
|
||||
print('计划执行结束 {}\n'.format(local_now_display()))
|
||||
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.save()
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
- name: Test privileged account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
@@ -12,9 +13,14 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
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
|
||||
|
||||
- name: Change asset password
|
||||
- name: Change asset password (paramiko)
|
||||
custom_command:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
@@ -22,6 +28,11 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
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 }}"
|
||||
password: "{{ account.secret }}"
|
||||
commands: "{{ params.commands }}"
|
||||
@@ -30,9 +41,10 @@
|
||||
when: ping_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
- name: Verify password (paramiko)
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
become: false
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
- name: "Test privileged {{ jms_account.username }} account"
|
||||
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:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
@@ -12,44 +43,54 @@
|
||||
ignore_errors: true
|
||||
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
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- 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
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} password (paramiko)"
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -4,9 +4,58 @@ category: host
|
||||
type:
|
||||
- AIX
|
||||
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:
|
||||
AIX account change secret:
|
||||
zh: 使用 Ansible 模块 user 执行账号改密 (DES)
|
||||
ja: Ansible user モジュールを使用してアカウントのパスワード変更 (DES)
|
||||
en: Using Ansible module user to change account secret (DES)
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (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
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
- name: "Test privileged {{ jms_account.username }} account"
|
||||
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:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
@@ -12,11 +43,6 @@
|
||||
ignore_errors: true
|
||||
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
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
@@ -26,30 +52,45 @@
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- 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
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} password (paramiko)"
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -5,9 +5,59 @@ type:
|
||||
- unix
|
||||
- linux
|
||||
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:
|
||||
Posix account change secret:
|
||||
zh: 使用 Ansible 模块 user 执行账号改密 (SHA512)
|
||||
ja: Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)
|
||||
en: Using Ansible module user to change account secret (SHA512)
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
|
||||
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (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:
|
||||
# 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
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
groups: "{{ user_info.groups[0].name }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
|
||||
@@ -5,9 +5,22 @@ method: change_secret
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret:
|
||||
zh: 使用 Ansible 模块 win_user 执行 Windows 账号改密
|
||||
ja: Ansible win_user モジュールを使用して Windows アカウントのパスワード変更
|
||||
en: Using Ansible module win_user to change Windows account secret
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密'
|
||||
ja: 'Ansible win_user モジュールを使用して Windows アカウントのパスワード変更'
|
||||
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
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
- name: "Test privileged {{ jms_account.username }} account"
|
||||
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:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
@@ -12,22 +19,26 @@
|
||||
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 user groups
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: Push user password
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
@@ -41,14 +52,14 @@
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Push SSH key
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Set sudo setting
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
@@ -56,25 +67,31 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} password (paramiko)"
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
- name: "Test privileged {{ jms_account.username }} account"
|
||||
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:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
@@ -12,19 +19,23 @@
|
||||
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 user groups
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: Push user password
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
@@ -41,14 +52,14 @@
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Push SSH key
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Set sudo setting
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
@@ -56,25 +67,31 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} password (paramiko)"
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_shell_type: sh
|
||||
ansible_connection: local
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
ssh_ping:
|
||||
- name: Verify account (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
- name: Verify account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
@@ -12,3 +13,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
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 .automation import *
|
||||
from .vault import *
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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):
|
||||
@@ -7,12 +7,18 @@ class SecretType(TextChoices):
|
||||
SSH_KEY = 'ssh_key', _('SSH key')
|
||||
ACCESS_KEY = 'access_key', _('Access key')
|
||||
TOKEN = 'token', _('Token')
|
||||
API_KEY = 'api_key', _("API key")
|
||||
|
||||
|
||||
class AliasAccount(TextChoices):
|
||||
ALL = '@ALL', _('All')
|
||||
INPUT = '@INPUT', _('Manual input')
|
||||
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):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 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')
|
||||
username = drf_filters.CharFilter(field_name="username", 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')
|
||||
nodes = drf_filters.CharFilter(method='filter_nodes')
|
||||
node_id = drf_filters.CharFilter(method='filter_nodes')
|
||||
@@ -45,7 +46,7 @@ class AccountFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ['id', 'asset_id', 'source_id']
|
||||
fields = ['id', 'asset', 'source_id', 'secret_type']
|
||||
|
||||
|
||||
class GatheredAccountFilterSet(BaseFilterSet):
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# 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.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):
|
||||
@@ -29,13 +31,16 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('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')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
('secret_type', models.CharField(
|
||||
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')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||
('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)),
|
||||
('secret_type', models.CharField(
|
||||
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')),
|
||||
('version', models.IntegerField(default=0, verbose_name='Version')),
|
||||
('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')),
|
||||
('secret_type', models.CharField(
|
||||
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')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||
('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
|
||||
|
||||
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.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -53,7 +55,8 @@ class Migration(migrations.Migration):
|
||||
primary_key=True, serialize=False, to='assets.baseautomation')),
|
||||
('secret_type', models.CharField(
|
||||
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'),
|
||||
('random_one', 'All assets use the same random password'),
|
||||
('random_all',
|
||||
@@ -156,7 +159,8 @@ class Migration(migrations.Migration):
|
||||
primary_key=True, serialize=False, to='assets.baseautomation')),
|
||||
('secret_type', models.CharField(
|
||||
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'),
|
||||
('random_one', 'All assets use the same random password'),
|
||||
('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 .automations import *
|
||||
from .base import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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 simple_history.models import HistoricalRecords
|
||||
|
||||
from assets.models.base import AbsConnectivity
|
||||
from common.utils import lazyproperty
|
||||
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):
|
||||
@@ -32,7 +31,7 @@ class AccountHistoricalRecords(HistoricalRecords):
|
||||
diff = attrs - history_attrs
|
||||
if not diff:
|
||||
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):
|
||||
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")
|
||||
)
|
||||
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_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):
|
||||
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
|
||||
def versions(self):
|
||||
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):
|
||||
""" 排除自己和以自己为 su-from 的账号 """
|
||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount):
|
||||
su_from = models.ForeignKey(
|
||||
'self', related_name='su_to', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
)
|
||||
def replace_history_model_with_mixin():
|
||||
"""
|
||||
替换历史模型中的父类为指定的Mixin类。
|
||||
|
||||
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')),
|
||||
]
|
||||
Parameters:
|
||||
model (class): 历史模型类,例如 Account.history.model
|
||||
mixin_class (class): 要替换为的Mixin类
|
||||
|
||||
@classmethod
|
||||
def get_su_from_account_templates(cls, instance=None):
|
||||
if not instance:
|
||||
return cls.objects.all()
|
||||
return cls.objects.exclude(Q(id=instance.id) | Q(su_from=instance))
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
model = Account.history.model
|
||||
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):
|
||||
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)
|
||||
replace_history_model_with_mixin()
|
||||
|
||||
@@ -6,7 +6,7 @@ import uuid
|
||||
from celery import current_task
|
||||
from django.db import models
|
||||
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.db.encoder import ModelJSONFieldEncoder
|
||||
@@ -22,9 +22,13 @@ logger = get_logger(__file__)
|
||||
|
||||
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
types = models.JSONField(default=list)
|
||||
recipients = models.ManyToManyField(
|
||||
'users.User', related_name='recipient_escape_route_plans', blank=True,
|
||||
verbose_name=_("Recipient")
|
||||
recipients_part_one = models.ManyToManyField(
|
||||
'users.User', related_name='recipient_part_one_plans', blank=True,
|
||||
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):
|
||||
@@ -52,9 +56,13 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
'org_id': self.org_id,
|
||||
'created_by': self.created_by,
|
||||
'types': self.types,
|
||||
'recipients': {
|
||||
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
||||
for recipient in self.recipients.all()
|
||||
'recipients_part_one': {
|
||||
str(user.id): (str(user), bool(user.secret_key))
|
||||
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:
|
||||
hid = str(uuid.uuid4())
|
||||
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()
|
||||
|
||||
@@ -85,7 +93,7 @@ class AccountBackupExecution(OrgModelMixin):
|
||||
timedelta = models.FloatField(
|
||||
default=0.0, verbose_name=_('Time'), null=True
|
||||
)
|
||||
plan_snapshot = models.JSONField(
|
||||
snapshot = models.JSONField(
|
||||
encoder=ModelJSONFieldEncoder, default=dict,
|
||||
blank=True, null=True, verbose_name=_('Account backup snapshot')
|
||||
)
|
||||
@@ -108,16 +116,9 @@ class AccountBackupExecution(OrgModelMixin):
|
||||
|
||||
@property
|
||||
def types(self):
|
||||
types = self.plan_snapshot.get('types')
|
||||
types = self.snapshot.get('types')
|
||||
return types
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
recipients = self.plan_snapshot.get('recipients')
|
||||
if not recipients:
|
||||
return []
|
||||
return recipients.values()
|
||||
|
||||
@lazyproperty
|
||||
def backup_accounts(self):
|
||||
from accounts.models import Account
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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, SecretType, SecretStrategy, SSHKeyStrategy
|
||||
@@ -86,7 +86,7 @@ class ChangeSecretRecord(JMSBaseModel):
|
||||
asset = models.ForeignKey('assets.Asset', 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'))
|
||||
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_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
||||
status = models.CharField(max_length=16, default='pending')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.db import models
|
||||
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.models import Account
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.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 .base import AccountBaseAutomation
|
||||
|
||||
@@ -6,36 +6,35 @@ from hashlib import md5
|
||||
import sshpubkeys
|
||||
from django.conf import settings
|
||||
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 common.db import fields
|
||||
from common.utils import (
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||
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
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class BaseAccountQuerySet(models.QuerySet):
|
||||
class BaseAccountQuerySet(VaultQuerySetMixin, models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
|
||||
class BaseAccountManager(OrgManager):
|
||||
class BaseAccountManager(VaultManagerMixin, OrgManager):
|
||||
def active(self):
|
||||
return self.get_queryset().active()
|
||||
|
||||
|
||||
class BaseAccount(JMSOrgBaseModel):
|
||||
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
secret_type = models.CharField(
|
||||
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)
|
||||
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 users.models import User
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .account import *
|
||||
from .backup import *
|
||||
from .base import *
|
||||
from .template 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.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.generics import get_object_or_404
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
@@ -95,6 +95,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
field.name for field in template._meta.fields
|
||||
if field.name not in ignore_fields
|
||||
]
|
||||
field_names = [name if name != '_secret' else 'secret' for name in field_names]
|
||||
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
@@ -198,7 +200,6 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
|
||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||
asset = AccountAssetSerializer(label=_('Asset'))
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
source = LabeledChoiceField(
|
||||
choices=Source.choices, label=_("Source"), required=False,
|
||||
allow_null=True, default=Source.LOCAL
|
||||
@@ -233,6 +234,15 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
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):
|
||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||
state = serializers.CharField(read_only=True, label=_('State'))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- 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 accounts.models import AccountBackupAutomation, AccountBackupExecution
|
||||
@@ -24,7 +24,7 @@ class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSer
|
||||
]
|
||||
fields = read_only_fields + [
|
||||
'id', 'name', 'is_periodic', 'interval', 'crontab',
|
||||
'comment', 'recipients', 'types'
|
||||
'comment', 'types', 'recipients_part_one', 'recipients_part_two'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
@@ -44,7 +44,7 @@ class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AccountBackupExecution
|
||||
read_only_fields = [
|
||||
'id', 'date_start', 'timedelta', 'plan_snapshot',
|
||||
'trigger', 'reason', 'is_success', 'org_id', 'recipients'
|
||||
'id', 'date_start', 'timedelta', 'snapshot',
|
||||
'trigger', 'reason', 'is_success', 'org_id'
|
||||
]
|
||||
fields = read_only_fields + ['plan']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- 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 accounts.const import SecretType
|
||||
@@ -61,22 +61,23 @@ class AuthValidateMixin(serializers.Serializer):
|
||||
|
||||
|
||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = BaseAccount
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_small = fields_mini + [
|
||||
'secret_type', 'secret', 'has_secret', 'passphrase',
|
||||
'secret_type', 'secret', 'passphrase',
|
||||
'privileged', 'is_active', 'spec_info',
|
||||
]
|
||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||
fields = fields_small + fields_other
|
||||
read_only_fields = [
|
||||
'has_secret', 'spec_info',
|
||||
'date_verified', 'created_by', 'date_created',
|
||||
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'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 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 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 accounts.models import AutomationExecution
|
||||
@@ -63,15 +63,17 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
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 = {
|
||||
'type': tp,
|
||||
'name': obj.snapshot['name'],
|
||||
'comment': obj.snapshot['comment'],
|
||||
'accounts': obj.snapshot['accounts'],
|
||||
'node_amount': len(obj.snapshot['nodes']),
|
||||
'asset_amount': len(obj.snapshot['assets']),
|
||||
'type_display': getattr(AutomationTypes, tp).label,
|
||||
'name': obj.snapshot.get('name'),
|
||||
'comment': obj.snapshot.get('comment'),
|
||||
'accounts': obj.snapshot.get('accounts'),
|
||||
'node_amount': len(obj.snapshot.get('nodes', [])),
|
||||
'asset_amount': len(obj.snapshot.get('assets', [])),
|
||||
'type_display': type_display,
|
||||
}
|
||||
return snapshot
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- 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 accounts.const import (
|
||||
@@ -50,7 +50,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
|
||||
'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, **{
|
||||
'accounts': {'required': True},
|
||||
|
||||
@@ -10,7 +10,7 @@ class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
|
||||
|
||||
class Meta(ChangeSecretAutomationSerializer.Meta):
|
||||
model = PushAccountAutomation
|
||||
fields = ['params'] + [
|
||||
fields = [
|
||||
n for n in ChangeSecretAutomationSerializer.Meta.fields
|
||||
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 accounts.backends import vault_client
|
||||
from common.utils import get_logger
|
||||
from .models import Account
|
||||
from .models import Account, AccountTemplate
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -13,3 +14,23 @@ def on_account_pre_save(sender, instance, **kwargs):
|
||||
instance.version = 1
|
||||
else:
|
||||
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)
|
||||
def execute_account_backup_task(pid, trigger):
|
||||
def execute_account_backup_task(pid, trigger, **kwargs):
|
||||
from accounts.models import AccountBackupAutomation
|
||||
with tmp_to_root_org():
|
||||
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.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.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'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
||||
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(),
|
||||
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(),
|
||||
name='push-account-add-or-remove-node'),
|
||||
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 accounts.const import (
|
||||
SecretType, DEFAULT_PASSWORD_RULES
|
||||
)
|
||||
from common.utils import gen_key_pair, random_string
|
||||
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
||||
from common.utils import ssh_key_gen, random_string
|
||||
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
||||
|
||||
|
||||
@@ -16,7 +14,7 @@ class SecretGenerator:
|
||||
|
||||
@staticmethod
|
||||
def generate_ssh_key():
|
||||
private_key, public_key = gen_key_pair()
|
||||
private_key, public_key = ssh_key_gen()
|
||||
return private_key
|
||||
|
||||
def generate_password(self):
|
||||
@@ -41,6 +39,8 @@ def validate_password_for_ansible(password):
|
||||
# Ansible 推送的时候不支持
|
||||
if '{{' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||
if '{%' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{%` '))
|
||||
# Ansible Windows 推送的时候不支持
|
||||
if "'" in password:
|
||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
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
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -8,17 +7,20 @@ import common.db.fields
|
||||
|
||||
def migrate_users_login_acls(apps, schema_editor):
|
||||
login_acl_model = apps.get_model('acls', 'LoginACL')
|
||||
name_used = defaultdict(int)
|
||||
|
||||
for login_acl in login_acl_model.objects.all():
|
||||
name = login_acl.name
|
||||
if name_used[name] > 0:
|
||||
login_acl.name += "_{}".format(name_used[name])
|
||||
name_used[name] += 1
|
||||
name_used = []
|
||||
login_acls = []
|
||||
for login_acl in login_acl_model.objects.all().select_related('user'):
|
||||
name = '{}_{}'.format(login_acl.name, login_acl.user.username)
|
||||
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 = {
|
||||
"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):
|
||||
|
||||
@@ -7,6 +7,7 @@ from common.db.models import JMSBaseModel
|
||||
from common.utils import contains_ip
|
||||
from common.utils.time_period import contains_time_period
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from ..const import ActionChoices
|
||||
|
||||
__all__ = [
|
||||
'BaseACL', 'UserBaseACL', 'UserAssetAccountBaseACL',
|
||||
@@ -16,12 +17,6 @@ from orgs.utils import tmp_to_root_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):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import re
|
||||
|
||||
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 orgs.mixins.models import JMSOrgBaseModel
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.timezone import local_now_display
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -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 acls.models.base import ActionChoices, BaseACL
|
||||
from acls.models.base import BaseACL
|
||||
from common.serializers.fields import JSONManyToManyField, LabeledChoiceField
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from orgs.models import Organization
|
||||
from ..const import ActionChoices
|
||||
|
||||
common_help_text = _(
|
||||
"With * indicating a match all. "
|
||||
@@ -60,18 +61,21 @@ class ActionAclSerializer(serializers.Serializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_action_choices()
|
||||
|
||||
def set_action_choices(self):
|
||||
action = self.fields.get("action")
|
||||
if not action:
|
||||
return
|
||||
choices = action.choices
|
||||
if not has_valid_xpack_license():
|
||||
choices.pop(ActionChoices.review, None)
|
||||
action._choices = choices
|
||||
|
||||
|
||||
class BaserACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||
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
|
||||
fields_mini = ["id", "name"]
|
||||
fields_small = fields_mini + [
|
||||
@@ -84,6 +88,7 @@ class BaserACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||
extra_kwargs = {
|
||||
"priority": {"default": 50},
|
||||
"is_active": {"default": True},
|
||||
'reviewers': {'label': _('Recipients')},
|
||||
}
|
||||
|
||||
def validate_reviewers(self, reviewers):
|
||||
@@ -107,16 +112,16 @@ class BaserACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||
return valid_reviewers
|
||||
|
||||
|
||||
class BaserUserACLSerializer(BaserACLSerializer):
|
||||
class BaseUserACLSerializer(BaseACLSerializer):
|
||||
users = JSONManyToManyField(label=_('User'))
|
||||
|
||||
class Meta(BaserACLSerializer.Meta):
|
||||
fields = BaserACLSerializer.Meta.fields + ['users']
|
||||
class Meta(BaseACLSerializer.Meta):
|
||||
fields = BaseACLSerializer.Meta.fields + ['users']
|
||||
|
||||
|
||||
class BaseUserAssetAccountACLSerializer(BaserUserACLSerializer):
|
||||
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
|
||||
assets = JSONManyToManyField(label=_('Asset'))
|
||||
accounts = serializers.ListField(label=_('Account'))
|
||||
|
||||
class Meta(BaserUserACLSerializer.Meta):
|
||||
fields = BaserUserACLSerializer.Meta.fields + ['assets', 'accounts']
|
||||
class Meta(BaseUserACLSerializer.Meta):
|
||||
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 acls.models import CommandGroup, CommandFilterACL
|
||||
@@ -31,6 +31,8 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
||||
class Meta(BaseSerializer.Meta):
|
||||
model = CommandFilterACL
|
||||
fields = BaseSerializer.Meta.fields + ['command_groups']
|
||||
# 默认都支持所有的 actions
|
||||
action_choices_exclude = []
|
||||
|
||||
|
||||
class CommandReviewSerializer(serializers.Serializer):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||
from ..models import ConnectMethodACL
|
||||
from ..const import ActionChoices
|
||||
|
||||
__all__ = ["ConnectMethodACLSerializer"]
|
||||
|
||||
@@ -12,12 +13,6 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
||||
i for i in BaseSerializer.Meta.fields + ['connect_methods']
|
||||
if i not in ['assets', 'accounts']
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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)
|
||||
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
||||
ActionChoices.review, ActionChoices.accept
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.serializers import MethodSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .base import BaserUserACLSerializer
|
||||
from .base import BaseUserACLSerializer
|
||||
from .rules import RuleSerializer
|
||||
from ..models import LoginACL
|
||||
|
||||
@@ -11,12 +11,12 @@ __all__ = ["LoginACLSerializer"]
|
||||
common_help_text = _("With * indicating a match all. ")
|
||||
|
||||
|
||||
class LoginACLSerializer(BaserUserACLSerializer, BulkOrgResourceModelSerializer):
|
||||
class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
|
||||
rules = MethodSerializer(label=_('Rule'))
|
||||
|
||||
class Meta(BaserUserACLSerializer.Meta):
|
||||
class Meta(BaseUserACLSerializer.Meta):
|
||||
model = LoginACL
|
||||
fields = BaserUserACLSerializer.Meta.fields + ['rules', ]
|
||||
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
|
||||
|
||||
def get_rules_serializer(self):
|
||||
return RuleSerializer()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ApplicationsConfig(AppConfig):
|
||||
@@ -9,5 +9,4 @@ class ApplicationsConfig(AppConfig):
|
||||
verbose_name = _('Applications')
|
||||
|
||||
def ready(self):
|
||||
from . import signal_handlers
|
||||
super().ready()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_mysql.models
|
||||
import uuid
|
||||
|
||||
|
||||
@@ -127,7 +126,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
|
||||
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
|
||||
('attrs', django_mysql.models.JSONField(default=dict)),
|
||||
('attrs', models.JSONField(default=dict)),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 orgs.mixins.models import OrgModelMixin
|
||||
|
||||
@@ -3,6 +3,7 @@ from .cloud import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .gpt import *
|
||||
from .host import *
|
||||
from .permission import *
|
||||
from .web import *
|
||||
|
||||
@@ -82,7 +82,7 @@ class AssetFilterSet(BaseFilterSet):
|
||||
@staticmethod
|
||||
def filter_protocols(queryset, name, value):
|
||||
value = value.split(',')
|
||||
return queryset.filter(protocols__name__in=value)
|
||||
return queryset.filter(protocols__name__in=value).distinct()
|
||||
|
||||
@staticmethod
|
||||
def filter_labels(queryset, name, value):
|
||||
@@ -91,7 +91,7 @@ class AssetFilterSet(BaseFilterSet):
|
||||
queryset = queryset.filter(labels__name=n, labels__value=v)
|
||||
else:
|
||||
q = Q(labels__name__contains=value) | Q(labels__value__contains=value)
|
||||
queryset = queryset.filter(q)
|
||||
queryset = queryset.filter(q).distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -121,6 +121,14 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
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):
|
||||
cls = super().get_serializer_class()
|
||||
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.mixins import ListModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from assets.const import AllTypes
|
||||
from assets.serializers import CategorySerializer, TypeSerializer
|
||||
from common.api import JMSGenericViewSet
|
||||
from common.permissions import IsValidUser
|
||||
from assets.serializers import CategorySerializer, TypeSerializer
|
||||
from assets.const import AllTypes
|
||||
|
||||
__all__ = ['CategoryViewSet']
|
||||
|
||||
@@ -32,4 +32,3 @@ class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
|
||||
tp = request.query_params.get('type')
|
||||
constraints = AllTypes.get_constraints(category, tp)
|
||||
return Response(constraints)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ~*~ 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 rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView, Response
|
||||
@@ -26,6 +26,9 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
return serializers.DomainWithGatewaySerializer
|
||||
return serializers.DomainSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related('assets')
|
||||
|
||||
|
||||
class GatewayViewSet(HostViewSet):
|
||||
perm_model = Gateway
|
||||
|
||||
@@ -38,5 +38,6 @@ class LabelViewSet(OrgBulkModelViewSet):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List
|
||||
|
||||
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 common.utils import lazyproperty, timeit
|
||||
|
||||
@@ -42,7 +42,7 @@ class SerializeToTreeNodeMixin:
|
||||
'name': _name(node),
|
||||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'isParent': node.assets_amount > 0,
|
||||
'open': _open(node),
|
||||
'meta': {
|
||||
'data': {
|
||||
@@ -70,25 +70,18 @@ class SerializeToTreeNodeMixin:
|
||||
|
||||
@timeit
|
||||
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:
|
||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||
else:
|
||||
get_pid = lambda asset: node_key
|
||||
ssh_asset_ids = [
|
||||
str(i) for i in
|
||||
Protocol.objects.filter(name='ssh').values_list('asset_id', flat=True)
|
||||
]
|
||||
sftp_asset_ids = Protocol.objects.filter(name='sftp') \
|
||||
.values_list('asset_id', flat=True)
|
||||
sftp_asset_ids = list(sftp_asset_ids)
|
||||
data = [
|
||||
{
|
||||
'id': str(asset.id),
|
||||
'name': asset.name,
|
||||
'title':
|
||||
f'{asset.address}\n{asset.comment}'
|
||||
if asset.comment else asset.address,
|
||||
'title': f'{asset.address}\n{asset.comment}',
|
||||
'pId': pid or get_pid(asset),
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
@@ -99,8 +92,7 @@ class SerializeToTreeNodeMixin:
|
||||
'data': {
|
||||
'platform_type': asset.platform.type,
|
||||
'org_name': asset.org_name,
|
||||
'sftp': (asset.platform_id in sftp_enabled_platform) \
|
||||
and (str(asset.id) in ssh_asset_ids),
|
||||
'sftp': asset.id in sftp_asset_ids,
|
||||
'name': asset.name,
|
||||
'address': asset.address
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import namedtuple, defaultdict
|
||||
from functools import partial
|
||||
|
||||
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.decorators import action
|
||||
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 assets.const import AllTypes
|
||||
from assets.models import Platform, Node, Asset
|
||||
from assets.serializers import PlatformSerializer
|
||||
from assets.models import Platform, Node, Asset, PlatformProtocol
|
||||
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidUser
|
||||
from common.serializers import GroupedChoiceSerializer
|
||||
|
||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi']
|
||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet']
|
||||
|
||||
|
||||
class AssetPlatformViewSet(JMSModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_classes = {
|
||||
'default': PlatformSerializer,
|
||||
'categories': GroupedChoiceSerializer
|
||||
'categories': GroupedChoiceSerializer,
|
||||
}
|
||||
filterset_fields = ['name', 'category', 'type']
|
||||
search_fields = ['name']
|
||||
@@ -25,7 +25,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
'categories': 'assets.view_platform',
|
||||
'type_constraints': 'assets.view_platform',
|
||||
'ops_methods': 'assets.view_platform',
|
||||
'filter_nodes_assets': 'assets.view_platform'
|
||||
'filter_nodes_assets': 'assets.view_platform',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -61,6 +61,15 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
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):
|
||||
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