mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68135f3b79 | ||
|
|
eedc2f1b41 | ||
|
|
7ba24293d1 | ||
|
|
f10114c9ed | ||
|
|
cf31cbfb07 | ||
|
|
f0ffa2408d | ||
|
|
b557e264bc | ||
|
|
457d2b2359 | ||
|
|
8ebc99339b | ||
|
|
e71e335f5c | ||
|
|
7517e77af9 | ||
|
|
889cdca3b0 | ||
|
|
4cfd1bc047 | ||
|
|
fc0891ceee | ||
|
|
cea16fc41f | ||
|
|
4b7c0b8437 | ||
|
|
09432b01a7 | ||
|
|
d7f8ba58ad | ||
|
|
f660c38d80 | ||
|
|
edf0630cef | ||
|
|
c4342567ba | ||
|
|
d4e53be7ce | ||
|
|
d4721e90d5 | ||
|
|
bb6c6c8f6a | ||
|
|
753ab77c46 | ||
|
|
ba127c506d | ||
|
|
c21ca70158 | ||
|
|
135fb7c6f9 | ||
|
|
f592f19b08 | ||
|
|
dce68cd011 | ||
|
|
d7b1903fb7 | ||
|
|
6e506e3146 | ||
|
|
58d30e7f85 | ||
|
|
2062778ab8 | ||
|
|
eaca296bd0 | ||
|
|
1051c6af04 | ||
|
|
aa69353474 | ||
|
|
d1f31f078b | ||
|
|
be80663436 | ||
|
|
1ae363d6bd | ||
|
|
31b0d345ad | ||
|
|
cabda0a32f | ||
|
|
f606dd8920 | ||
|
|
973df0360c | ||
|
|
f9f1d96674 | ||
|
|
0edad24d5d | ||
|
|
8cb74976e1 | ||
|
|
279109c9a6 | ||
|
|
8c7ba4a497 | ||
|
|
9cc048267b | ||
|
|
78d0e3f485 | ||
|
|
8aefacd7ed | ||
|
|
ef8db68db1 | ||
|
|
00256f86df | ||
|
|
77569c554b | ||
|
|
7897462e32 | ||
|
|
aee11827c4 | ||
|
|
a6bf592046 | ||
|
|
1dea424104 | ||
|
|
1f5554d945 | ||
|
|
0303408be8 | ||
|
|
f5802ace02 | ||
|
|
8bde45d9dc | ||
|
|
e8bbc44647 | ||
|
|
34aa48d18c | ||
|
|
1f1c1a9157 | ||
|
|
7aa6613e69 | ||
|
|
503034299e | ||
|
|
6c9d271ae1 | ||
|
|
6ff852e225 | ||
|
|
0c74e92bfb | ||
|
|
3853d0bcc6 | ||
|
|
cd0348cca1 | ||
|
|
ce94348d45 | ||
|
|
f74f8b7d8c | ||
|
|
dc79346bdc | ||
|
|
37a0d831da | ||
|
|
e509568fe5 | ||
|
|
baa75dc735 | ||
|
|
8a9f0436b8 | ||
|
|
2c2c3eb21a | ||
|
|
18681d1f50 | ||
|
|
86ef984c02 | ||
|
|
e4d8ce097a | ||
|
|
ae68241812 | ||
|
|
13d4177531 | ||
|
|
641e75a905 | ||
|
|
a2d6e41816 | ||
|
|
6cd3672604 | ||
|
|
3c3c1499b7 | ||
|
|
e29e51121d | ||
|
|
fabee37e9e | ||
|
|
2994ea6f68 | ||
|
|
644eada8a1 | ||
|
|
000a3038e1 | ||
|
|
9c8635b230 | ||
|
|
e428eb351b | ||
|
|
1275087f19 | ||
|
|
311c01242b | ||
|
|
bab5b67c52 | ||
|
|
3eb0b768a6 | ||
|
|
6dcc74a388 | ||
|
|
2b15fc5e8b | ||
|
|
df655f304a | ||
|
|
25223719cb | ||
|
|
814dbeb749 | ||
|
|
630bb56601 | ||
|
|
496b72aaee | ||
|
|
b57e943990 | ||
|
|
b4c1dd2944 | ||
|
|
9ede3670a7 | ||
|
|
2a29cd0e70 | ||
|
|
15ac81a422 | ||
|
|
eb5a53b91b | ||
|
|
4dd72b109f | ||
|
|
2fcbfe9f21 | ||
|
|
e80a0e41ba | ||
|
|
7cdba3ef38 | ||
|
|
2d6e815b3d | ||
|
|
38642024be | ||
|
|
257ee205ac | ||
|
|
a9620a3cbe | ||
|
|
769e7dc8a0 | ||
|
|
4b961a626b | ||
|
|
2a70449411 | ||
|
|
653a6752b6 | ||
|
|
32255c6077 | ||
|
|
7a708156ee | ||
|
|
b72a446bbd | ||
|
|
219fad9b62 | ||
|
|
6c1c8b241e | ||
|
|
a4d0e3fd17 | ||
|
|
af44ffab0a | ||
|
|
a09b7b29e2 | ||
|
|
8f67922c80 | ||
|
|
f1db5d6f44 | ||
|
|
33ea5eb41f | ||
|
|
48bcbc6c53 | ||
|
|
3e090eb701 | ||
|
|
6ac956c626 | ||
|
|
edb2d1bd7b | ||
|
|
81b4909016 | ||
|
|
f6f1be423c | ||
|
|
fae5392a03 | ||
|
|
d5224968bc | ||
|
|
6565f8c0a8 | ||
|
|
8df720f19e | ||
|
|
bc5494bbb0 | ||
|
|
febf08629a | ||
|
|
b6774aa749 | ||
|
|
bc668f3e9f | ||
|
|
dc56b019b1 | ||
|
|
a38624d198 | ||
|
|
ca026040fe | ||
|
|
88b9a4d693 | ||
|
|
4d15e46ceb | ||
|
|
55575e9f7f | ||
|
|
98c9cddcbf | ||
|
|
9f67ba573c | ||
|
|
533f13c634 | ||
|
|
c66b1db784 | ||
|
|
d03ba7c391 | ||
|
|
6544f8ade8 | ||
|
|
ac5991fc43 | ||
|
|
9b2b71dddc | ||
|
|
e18e019460 | ||
|
|
ef1875d9b5 | ||
|
|
0b7552a6ee | ||
|
|
45425b11d2 | ||
|
|
fda3e6ec9b | ||
|
|
2b41486f2a | ||
|
|
59d9a3d4ec | ||
|
|
3c7ba029dd | ||
|
|
1335556272 | ||
|
|
8eab87f40d | ||
|
|
c441e5bb92 | ||
|
|
da8d78f384 | ||
|
|
83b91cb739 | ||
|
|
1afad40dd3 | ||
|
|
1358cf532f | ||
|
|
1e7f268f0c | ||
|
|
d6b5590505 | ||
|
|
79b3b31492 | ||
|
|
4f2b3fbb43 | ||
|
|
1f2db65dba | ||
|
|
006faac326 | ||
|
|
f7fee0f430 | ||
|
|
714c44fbf4 | ||
|
|
84b316e2c1 | ||
|
|
6955a3db11 | ||
|
|
d92736e624 | ||
|
|
9d0da64ea1 | ||
|
|
b9e1d6093e | ||
|
|
c3820b30b8 | ||
|
|
6955fc1734 | ||
|
|
32178b2344 | ||
|
|
e3c0518cfb | ||
|
|
438e9dee2a | ||
|
|
3c9239eb09 | ||
|
|
81fb080c67 | ||
|
|
6cf05435bf | ||
|
|
65718c5a84 | ||
|
|
27daebbe1b | ||
|
|
dce1079fdc | ||
|
|
d07db68426 | ||
|
|
6d37300a30 | ||
|
|
0c96af32c2 | ||
|
|
1c6b1b0625 | ||
|
|
4f7b4842f6 | ||
|
|
c4fef5899c | ||
|
|
5b51a8231c | ||
|
|
54417dd6d3 | ||
|
|
2c7ad90524 | ||
|
|
01fcdad489 | ||
|
|
8801003461 | ||
|
|
696397fdb0 | ||
|
|
87a24991f1 | ||
|
|
3ec93b8f04 | ||
|
|
4f1826d3ed | ||
|
|
9260f26c99 | ||
|
|
93da3e58f2 | ||
|
|
1eff33f3f7 | ||
|
|
8e89d42343 | ||
|
|
d0b0c87d3c | ||
|
|
e3ac26e377 | ||
|
|
4ea20a9103 | ||
|
|
dd57b14562 | ||
|
|
c312cdb625 | ||
|
|
85fedf0704 | ||
|
|
8b05260a6c | ||
|
|
47cb6b1ec0 | ||
|
|
79b5dff210 | ||
|
|
b08e1f6a47 | ||
|
|
2e3184cbd6 | ||
|
|
fb903e53a4 | ||
|
|
cc7220a4ad | ||
|
|
81de527e32 | ||
|
|
7ad2abe104 | ||
|
|
9a2da98bd4 | ||
|
|
eca50874f0 | ||
|
|
8f82ca9856 | ||
|
|
e193d7a942 | ||
|
|
d2429f7883 | ||
|
|
a43bb25b5a | ||
|
|
ffe3e8a70c | ||
|
|
0e7e499a1e | ||
|
|
e812e3ff89 | ||
|
|
d2eacad97b | ||
|
|
8291a81efd | ||
|
|
a91cb1afd5 | ||
|
|
2cad97065f | ||
|
|
cf18300360 | ||
|
|
3cd22f05d2 | ||
|
|
eee41008cc | ||
|
|
0fdae00722 | ||
|
|
575562c416 | ||
|
|
e2b7f67fdc | ||
|
|
d2498c0d53 | ||
|
|
01e40fd238 | ||
|
|
370ef11486 | ||
|
|
089cadeae3 | ||
|
|
6b748e5ac5 | ||
|
|
6d611bbbbd | ||
|
|
18670d493e | ||
|
|
ba38852354 | ||
|
|
64f3509c8c | ||
|
|
805c78c0de | ||
|
|
11accf8854 | ||
|
|
18f6ffe0ce | ||
|
|
6b7119ea74 | ||
|
|
efc7ca1164 | ||
|
|
a6de9bdde6 | ||
|
|
6e7074ba40 | ||
|
|
2edcb2f2d3 | ||
|
|
07e1918fa1 | ||
|
|
452b383278 | ||
|
|
ed92f10208 | ||
|
|
e8331ca708 | ||
|
|
814130204a | ||
|
|
e7dc9a2f6f | ||
|
|
dabbb45f6e | ||
|
|
ded1b4bba1 | ||
|
|
2630ea39a1 | ||
|
|
9e10029bdd | ||
|
|
d1391cb5d5 | ||
|
|
44f029774d | ||
|
|
23fce9e426 | ||
|
|
0778a39894 | ||
|
|
9cc6d6a9af | ||
|
|
8f309dee92 | ||
|
|
d166b26252 | ||
|
|
1ef51563b5 | ||
|
|
3e7b4682e4 | ||
|
|
994b42aa93 | ||
|
|
d6aea54722 | ||
|
|
88afabdd1d | ||
|
|
b2327c0c5a | ||
|
|
7610f64433 | ||
|
|
b15c314384 | ||
|
|
7a5cffac91 | ||
|
|
8667943443 | ||
|
|
7c51d90a3d | ||
|
|
9996b200f9 | ||
|
|
ae364ac373 | ||
|
|
fef4a97931 | ||
|
|
d63c4d6cc4 | ||
|
|
4e5a44bd98 | ||
|
|
fcce03f7bd | ||
|
|
5f121934a7 | ||
|
|
521c1f0dfa | ||
|
|
5673698a57 | ||
|
|
d6b75ac700 | ||
|
|
0ee14e6d85 | ||
|
|
9babe977d8 | ||
|
|
0f9223331c | ||
|
|
f8a4a0e108 | ||
|
|
ba76f30af9 | ||
|
|
e5e0c841a2 | ||
|
|
c41fdf1786 | ||
|
|
69c0eb2f50 | ||
|
|
e077afe2cc | ||
|
|
c1f572df05 | ||
|
|
d60fe464ca | ||
|
|
f47895b8a8 | ||
|
|
3eb1583c69 | ||
|
|
5ab8ff4fde | ||
|
|
7746491e19 | ||
|
|
5e54792d94 | ||
|
|
621c7a31fe | ||
|
|
75bab70ccf | ||
|
|
30683ed859 | ||
|
|
7c52cec5fb | ||
|
|
f01bfc44b8 | ||
|
|
54b89f6fee | ||
|
|
c0de0b0d8e | ||
|
|
06275a09ac | ||
|
|
7b86938b58 | ||
|
|
44624d0ce0 | ||
|
|
9b8c817a16 | ||
|
|
927fe1f128 | ||
|
|
eee119eba1 | ||
|
|
53d8f716eb | ||
|
|
f48aec2bcb | ||
|
|
78e9f51786 | ||
|
|
af33ad6631 | ||
|
|
864da49ae6 | ||
|
|
e6b8b3982d | ||
|
|
49b3df218e | ||
|
|
0858d67098 | ||
|
|
ffa242e635 | ||
|
|
4021b1955e | ||
|
|
204258f058 | ||
|
|
dc841650cf | ||
|
|
bc54685a31 | ||
|
|
ee586954f8 | ||
|
|
e56a37afd2 | ||
|
|
7669744312 | ||
|
|
ad8aba88a3 | ||
|
|
7659846df4 | ||
|
|
f93979eb2d | ||
|
|
badf83c560 | ||
|
|
f6466a3a20 | ||
|
|
996394ba29 | ||
|
|
09f8470d34 | ||
|
|
fdb3f6409c | ||
|
|
73b0b23910 | ||
|
|
c1185e989a | ||
|
|
1239082649 | ||
|
|
ff073185f1 | ||
|
|
d7a682b462 | ||
|
|
4df2bdd9b6 | ||
|
|
2437072768 | ||
|
|
08a2d96213 | ||
|
|
de7d7b41c0 | ||
|
|
b04c7f022f | ||
|
|
bf0d9f4b80 | ||
|
|
314257f790 | ||
|
|
6d2a62e413 | ||
|
|
1734ddc2bd | ||
|
|
7c796e8201 | ||
|
|
62a74418ea | ||
|
|
32461078fe | ||
|
|
939b517e34 | ||
|
|
66eac762ff | ||
|
|
6f4082f800 | ||
|
|
edd65f965b |
4
.github/workflows/jms-build-test.yml
vendored
4
.github/workflows/jms-build-test.yml
vendored
@@ -19,8 +19,8 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: jumpserver/core:test
|
||||
file: Dockerfile
|
||||
tags: jumpserver/core-ce:test
|
||||
file: Dockerfile-ce
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim-bullseye as stage-build
|
||||
FROM python:3.11-slim-bullseye as stage-1
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
@@ -6,9 +6,10 @@ ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
FROM python:3.11-slim-bullseye as stage-2
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
@@ -18,11 +19,11 @@ ARG BUILD_DEPENDENCIES=" \
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
@@ -31,6 +32,54 @@ ARG DEPENDENCIES=" \
|
||||
freerdp2-dev \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
git \
|
||||
git-lfs \
|
||||
unzip \
|
||||
xz-utils \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
ENV LANG=zh_CN.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
libjpeg-dev \
|
||||
libpq-dev \
|
||||
libx11-dev \
|
||||
freerdp2-dev \
|
||||
libxmlsec1-openssl"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
@@ -47,39 +96,30 @@ ARG TOOLS=" \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& 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/*
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||
|
||||
COPY --from=stage-2 /opt/py3 /opt/py3
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
set -ex \
|
||||
&& echo > /opt/jumpserver/config.yml \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& poetry install --only=main
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
ARG VERSION
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||
FROM jumpserver/core:${VERSION}
|
||||
FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
|
||||
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
set -ex \
|
||||
&& poetry install --only=xpack
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
@@ -61,6 +61,7 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
|
||||
## 案例研究
|
||||
|
||||
- [腾讯音乐娱乐集团:基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
|
||||
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
|
||||
- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
|
||||
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
|
||||
@@ -93,11 +94,12 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
| [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 项目 |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Linux) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
|
||||
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
|
||||
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core Api 通信的组件项目 |
|
||||
| [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 安装包 项目 |
|
||||
|
||||
@@ -111,7 +113,7 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
|
||||
|
||||
## License & Copyright
|
||||
|
||||
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.
|
||||
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in
|
||||
compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from django.db.models import Q
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.tasks import verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from accounts.models import Account
|
||||
from accounts.permissions import AccountTaskActionPermission
|
||||
from accounts.tasks import (
|
||||
remove_accounts_task, verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
)
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
|
||||
__all__ = [
|
||||
'AccountsTaskCreateAPI',
|
||||
@@ -12,40 +16,48 @@ __all__ = [
|
||||
|
||||
class AccountsTaskCreateAPI(CreateAPIView):
|
||||
serializer_class = serializers.AccountTaskSerializer
|
||||
permission_classes = (AccountTaskActionPermission,)
|
||||
|
||||
def check_permissions(self, request):
|
||||
act = request.data.get('action')
|
||||
if act == 'push':
|
||||
code = 'accounts.push_account'
|
||||
else:
|
||||
code = 'accounts.verify_account'
|
||||
has = request.user.has_perm(code)
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
def get_permissions(self):
|
||||
act = self.request.data.get('action')
|
||||
if act == 'remove':
|
||||
self.permission_classes = [
|
||||
AccountTaskActionPermission,
|
||||
UserConfirmation.require(ConfirmType.PASSWORD)
|
||||
]
|
||||
return super().get_permissions()
|
||||
|
||||
@staticmethod
|
||||
def get_account_ids(data, action):
|
||||
account_type = 'gather_accounts' if action == 'remove' else 'accounts'
|
||||
accounts = data.get(account_type, [])
|
||||
account_ids = [str(a.id) for a in accounts]
|
||||
|
||||
if action == 'remove':
|
||||
return account_ids
|
||||
|
||||
assets = data.get('assets', [])
|
||||
asset_ids = [str(a.id) for a in assets]
|
||||
ids = Account.objects.filter(
|
||||
Q(id__in=account_ids) | Q(asset_id__in=asset_ids)
|
||||
).distinct().values_list('id', flat=True)
|
||||
return [str(_id) for _id in ids]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
accounts = data.get('accounts', [])
|
||||
params = data.get('params')
|
||||
account_ids = [str(a.id) for a in accounts]
|
||||
action = data['action']
|
||||
ids = self.get_account_ids(data, action)
|
||||
|
||||
if data['action'] == 'push':
|
||||
task = push_accounts_to_assets_task.delay(account_ids, params)
|
||||
if action == 'push':
|
||||
task = push_accounts_to_assets_task.delay(ids, data.get('params'))
|
||||
elif action == 'remove':
|
||||
task = remove_accounts_task.delay(ids)
|
||||
elif action == 'verify':
|
||||
task = verify_accounts_connectivity_task.delay(ids)
|
||||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
raise ValueError(f"Invalid action: {action}")
|
||||
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=401)
|
||||
|
||||
return handler
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import mixins
|
||||
from rest_framework import status, mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
||||
from accounts.tasks import execute_automation_record_task
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from .base import (
|
||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||
@@ -29,18 +31,27 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||
|
||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
filter_fields = ('asset', 'execution_id')
|
||||
filterset_fields = ('asset_id', 'execution_id')
|
||||
search_fields = ('asset__address',)
|
||||
tp = AutomationTypes.change_secret
|
||||
rbac_perms = {
|
||||
'execute': 'accounts.add_changesecretexecution',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.filter(
|
||||
execution__automation__type=AutomationTypes.change_secret
|
||||
)
|
||||
return ChangeSecretRecord.objects.all()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
eid = self.request.query_params.get('execution_id')
|
||||
return queryset.filter(execution_id=eid)
|
||||
@action(methods=['post'], detail=False, url_path='execute')
|
||||
def execute(self, request, *args, **kwargs):
|
||||
record_id = request.data.get('record_id')
|
||||
record = self.get_queryset().filter(pk=record_id)
|
||||
if not record:
|
||||
return Response(
|
||||
{'detail': 'record not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
task = execute_automation_record_task.delay(record_id, self.tp)
|
||||
return Response({'task': task.id}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||
|
||||
@@ -42,6 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
|
||||
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
tp = AutomationTypes.push_account
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.filter(
|
||||
|
||||
@@ -3,19 +3,26 @@ import time
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from openpyxl import Workbook
|
||||
from rest_framework import serializers
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.notifications import AccountBackupExecutionTaskMsg
|
||||
from accounts.const.automation import AccountBackupType
|
||||
from accounts.models.automations.backup_account import AccountBackupAutomation
|
||||
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
||||
from accounts.serializers import AccountSecretSerializer
|
||||
from assets.const import AllTypes
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
from common.utils.timezone import local_now_display
|
||||
from common.utils.file import encrypt_and_compress_zip_file, zip_files
|
||||
from common.utils.timezone import local_now_filename, local_now_display
|
||||
from terminal.models.component.storage import ReplayStorage
|
||||
from users.models import User
|
||||
|
||||
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||
|
||||
|
||||
class RecipientsNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseAccountHandler:
|
||||
@classmethod
|
||||
def unpack_data(cls, serializer_data, data=None):
|
||||
@@ -67,7 +74,7 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
@staticmethod
|
||||
def get_filename(plan_name):
|
||||
filename = os.path.join(
|
||||
PATH, f'{plan_name}-{local_now_display()}-{time.time()}.xlsx'
|
||||
PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.xlsx'
|
||||
)
|
||||
return filename
|
||||
|
||||
@@ -137,13 +144,14 @@ class AccountBackupHandler:
|
||||
|
||||
wb = Workbook(filename)
|
||||
for sheet, data in data_map.items():
|
||||
ws = wb.create_sheet(str(sheet))
|
||||
for row in data:
|
||||
ws.append(row)
|
||||
wb.save(filename)
|
||||
ws = wb.add_worksheet(str(sheet))
|
||||
for row_index, row_data in enumerate(data):
|
||||
for col_index, col_data in enumerate(row_data):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
files.append(filename)
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
print('步骤完成: 用时 {}s'.format(timedelta))
|
||||
print('创建备份文件完成: 用时 {}s'.format(timedelta))
|
||||
return files
|
||||
|
||||
def send_backup_mail(self, files, recipients):
|
||||
@@ -152,7 +160,7 @@ class AccountBackupHandler:
|
||||
recipients = User.objects.filter(id__in=list(recipients))
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 发送备份邮件\033[0m'
|
||||
'\033[32m>>> 开始发送备份邮件\033[0m'
|
||||
''
|
||||
)
|
||||
plan_name = self.plan_name
|
||||
@@ -160,20 +168,42 @@ class AccountBackupHandler:
|
||||
if not user.secret_key:
|
||||
attachment_list = []
|
||||
else:
|
||||
password = user.secret_key.encode('utf8')
|
||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_display()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, password, files)
|
||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, user.secret_key, files)
|
||||
attachment_list = [attachment, ]
|
||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
||||
print('邮件已发送至{}({})'.format(user, user.email))
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
def send_backup_obj_storage(self, files, recipients, password):
|
||||
if not files:
|
||||
return
|
||||
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
|
||||
print(
|
||||
'\n'
|
||||
'\033[32m>>> 开始发送备份文件到sftp服务器\033[0m'
|
||||
''
|
||||
)
|
||||
plan_name = self.plan_name
|
||||
for rec in recipients:
|
||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
||||
if password:
|
||||
print('\033[32m>>> 使用加密密码对文件进行加密中\033[0m')
|
||||
encrypt_and_compress_zip_file(attachment, password, files)
|
||||
else:
|
||||
zip_files(attachment, files)
|
||||
attachment_list = attachment
|
||||
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list)
|
||||
print('备份文件将发送至{}({})'.format(rec.name, rec.id))
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
def step_perform_task_update(self, is_success, reason):
|
||||
self.execution.reason = reason[:1024]
|
||||
self.execution.is_success = is_success
|
||||
self.execution.save()
|
||||
print('已完成对任务状态的更新')
|
||||
print('\n已完成对任务状态的更新\n')
|
||||
|
||||
@staticmethod
|
||||
def step_finished(is_success):
|
||||
@@ -186,24 +216,11 @@ class AccountBackupHandler:
|
||||
is_success = False
|
||||
error = '-'
|
||||
try:
|
||||
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)
|
||||
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value)
|
||||
if backup_type == AccountBackupType.email.value:
|
||||
self.backup_by_email()
|
||||
elif backup_type == AccountBackupType.object_storage.value:
|
||||
self.backup_by_obj_storage()
|
||||
except Exception as e:
|
||||
self.is_frozen = True
|
||||
print('任务执行被异常中断')
|
||||
@@ -217,6 +234,52 @@ class AccountBackupHandler:
|
||||
self.step_perform_task_update(is_success, reason)
|
||||
self.step_finished(is_success)
|
||||
|
||||
def backup_by_obj_storage(self):
|
||||
object_id = self.execution.snapshot.get('id')
|
||||
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password
|
||||
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
|
||||
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
|
||||
if not obj_recipients_part_one and not obj_recipients_part_two:
|
||||
print(
|
||||
'\n'
|
||||
'\033[31m>>> 该备份任务未分配sftp服务器\033[0m'
|
||||
''
|
||||
)
|
||||
raise RecipientsNotFound('Not Found Recipients')
|
||||
if obj_recipients_part_one and obj_recipients_part_two:
|
||||
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
|
||||
files = self.create_excel(section='front')
|
||||
self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password)
|
||||
|
||||
files = self.create_excel(section='back')
|
||||
self.send_backup_obj_storage(files, obj_recipients_part_two, zip_encrypt_password)
|
||||
else:
|
||||
recipients = obj_recipients_part_one or obj_recipients_part_two
|
||||
files = self.create_excel()
|
||||
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
|
||||
|
||||
def backup_by_email(self):
|
||||
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||
if not recipients_part_one and not recipients_part_two:
|
||||
print(
|
||||
'\n'
|
||||
'\033[31m>>> 该备份任务未分配收件人\033[0m'
|
||||
''
|
||||
)
|
||||
raise RecipientsNotFound('Not Found Recipients')
|
||||
if recipients_part_one and recipients_part_two:
|
||||
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
|
||||
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)
|
||||
|
||||
def run(self):
|
||||
print('任务开始: {}'.format(local_now_display()))
|
||||
time_start = time.time()
|
||||
@@ -229,4 +292,4 @@ class AccountBackupHandler:
|
||||
finally:
|
||||
print('\n任务结束: {}'.format(local_now_display()))
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
print('用时: {}'.format(timedelta))
|
||||
print('用时: {}s'.format(timedelta))
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
become_password: "{{ custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||
register: ping_info
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Change asset password (paramiko)
|
||||
custom_command:
|
||||
@@ -40,6 +41,7 @@
|
||||
ignore_errors: true
|
||||
when: ping_info is succeeded
|
||||
register: change_info
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify password (paramiko)
|
||||
ssh_ping:
|
||||
@@ -52,3 +54,4 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -6,15 +6,27 @@ category:
|
||||
type:
|
||||
- all
|
||||
method: change_secret
|
||||
protocol: ssh
|
||||
priority: 50
|
||||
params:
|
||||
- name: commands
|
||||
type: list
|
||||
label: '自定义命令'
|
||||
label: "{{ 'Params commands label' | trans }}"
|
||||
default: [ '' ]
|
||||
help_text: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
help_text: "{{ 'Params commands help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
SSH account change secret:
|
||||
zh: 使用 SSH 命令行自定义改密
|
||||
ja: SSH コマンドライン方式でカスタムパスワード変更
|
||||
en: Custom password change by SSH command line
|
||||
zh: '使用 SSH 命令行自定义改密'
|
||||
ja: 'SSH コマンドライン方式でカスタムパスワード変更'
|
||||
en: 'Custom password change by SSH command line'
|
||||
|
||||
Params commands help text:
|
||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
||||
|
||||
Params commands label:
|
||||
zh: '自定义命令'
|
||||
ja: 'カスタムコマンド'
|
||||
en: 'Custom command'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -11,10 +12,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -28,10 +29,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -45,8 +46,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
@@ -39,3 +39,4 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
- name: Change password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
@@ -0,0 +1,27 @@
|
||||
id: change_secret_windows_rdp_verify
|
||||
name: "{{ 'Windows account change secret rdp verify' | trans }}"
|
||||
version: 1
|
||||
method: change_secret
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
priority: 49
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret rdp verify:
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密 RDP 协议测试最后的可连接性'
|
||||
ja: 'Ansibleモジュールwin_userはWindowsアカウントの改密RDPプロトコルテストの最後の接続性を実行する'
|
||||
en: 'Using the Ansible module win_user performs Windows account encryption RDP protocol testing for final connectivity'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from openpyxl import Workbook
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy
|
||||
from accounts.models import ChangeSecretRecord
|
||||
@@ -14,7 +14,7 @@ from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from assets.const import HostTypes
|
||||
from common.utils import get_logger
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
from common.utils.timezone import local_now_display
|
||||
from common.utils.timezone import local_now_filename
|
||||
from users.models import User
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
from ...utils import SecretGenerator
|
||||
@@ -27,7 +27,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.method_hosts_mapper = defaultdict(list)
|
||||
self.record_id = self.execution.snapshot.get('record_id')
|
||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
@@ -50,7 +50,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||
kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username)
|
||||
username = account.username
|
||||
path = f'/{username}' if username == "root" else f'/home/{username}'
|
||||
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
@@ -96,17 +98,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
if not accounts:
|
||||
print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % (
|
||||
print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % (
|
||||
asset.name, self.account_ids, self.secret_type
|
||||
))
|
||||
return []
|
||||
|
||||
method_attr = getattr(automation, self.method_type() + '_method')
|
||||
method_hosts = self.method_hosts_mapper[method_attr]
|
||||
method_hosts = [h for h in method_hosts if h != host['name']]
|
||||
inventory_hosts = []
|
||||
records = []
|
||||
|
||||
inventory_hosts = []
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
@@ -116,13 +114,24 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
h['name'] += '(' + account.username + ')'
|
||||
new_secret = self.get_secret(secret_type)
|
||||
if self.secret_type is None:
|
||||
new_secret = account.secret
|
||||
else:
|
||||
new_secret = self.get_secret(secret_type)
|
||||
|
||||
if new_secret is None:
|
||||
print(f'new_secret is None, account: {account}')
|
||||
continue
|
||||
|
||||
if self.record_id is None:
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
)
|
||||
records.append(recorder)
|
||||
else:
|
||||
recorder = ChangeSecretRecord.objects.get(id=self.record_id)
|
||||
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
)
|
||||
records.append(recorder)
|
||||
self.name_recorder_mapper[h['name']] = recorder
|
||||
|
||||
private_key_path = None
|
||||
@@ -135,14 +144,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
'secret': account.escape_jinja2_syntax(new_secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
inventory_hosts.append(h)
|
||||
method_hosts.append(h['name'])
|
||||
self.method_hosts_mapper[method_attr] = method_hosts
|
||||
ChangeSecretRecord.objects.bulk_create(records)
|
||||
return inventory_hosts
|
||||
|
||||
@@ -158,7 +166,8 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.secret = recorder.new_secret
|
||||
account.save(update_fields=['secret'])
|
||||
account.date_updated = timezone.now()
|
||||
account.save(update_fields=['secret', 'date_updated'])
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
@@ -170,7 +179,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
recorder.save()
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
logger.error("Change secret error: ", e)
|
||||
logger.error("Account error: ", e)
|
||||
|
||||
def check_secret(self):
|
||||
if self.secret_strategy == SecretStrategy.custom \
|
||||
@@ -179,15 +188,33 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_summary(recorders):
|
||||
total, succeed, failed = 0, 0, 0
|
||||
for recorder in recorders:
|
||||
if recorder.status == 'success':
|
||||
succeed += 1
|
||||
else:
|
||||
failed += 1
|
||||
total += 1
|
||||
|
||||
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
||||
return summary
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if not self.check_secret():
|
||||
if self.secret_type and not self.check_secret():
|
||||
return
|
||||
super().run(*args, **kwargs)
|
||||
recorders = self.name_recorder_mapper.values()
|
||||
recorders = list(recorders)
|
||||
self.send_recorder_mail(recorders)
|
||||
recorders = list(self.name_recorder_mapper.values())
|
||||
summary = self.get_summary(recorders)
|
||||
print(summary, end='')
|
||||
|
||||
def send_recorder_mail(self, recorders):
|
||||
if self.record_id:
|
||||
return
|
||||
|
||||
self.send_recorder_mail(recorders, summary)
|
||||
|
||||
def send_recorder_mail(self, recorders, summary):
|
||||
recipients = self.execution.recipients
|
||||
if not recorders or not recipients:
|
||||
return
|
||||
@@ -196,18 +223,17 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
|
||||
name = self.execution.snapshot['name']
|
||||
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||
filename = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.xlsx')
|
||||
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
|
||||
if not self.create_file(recorders, filename):
|
||||
return
|
||||
|
||||
for user in recipients:
|
||||
attachments = []
|
||||
if user.secret_key:
|
||||
password = user.secret_key.encode('utf8')
|
||||
attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, password, [filename])
|
||||
attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, user.secret_key, [filename])
|
||||
attachments = [attachment]
|
||||
ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
|
||||
ChangeSecretExecutionTaskMsg(name, user, summary).publish(attachments)
|
||||
os.remove(filename)
|
||||
|
||||
@staticmethod
|
||||
@@ -222,8 +248,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
|
||||
rows.insert(0, header)
|
||||
wb = Workbook(filename)
|
||||
ws = wb.create_sheet('Sheet1')
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
wb.save(filename)
|
||||
ws = wb.add_worksheet('Sheet1')
|
||||
for row_index, row_data in enumerate(rows):
|
||||
for col_index, col_data in enumerate(row_data):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
return True
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from .push_account.manager import PushAccountManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .verify_account.manager import VerifyAccountManager
|
||||
from .backup_account.manager import AccountBackupManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .gather_accounts.manager import GatherAccountsManager
|
||||
from .push_account.manager import PushAccountManager
|
||||
from .remove_account.manager import RemoveAccountManager
|
||||
from .verify_account.manager import VerifyAccountManager
|
||||
from .verify_gateway_account.manager import VerifyGatewayAccountManager
|
||||
from ..const import AutomationTypes
|
||||
|
||||
@@ -12,6 +13,7 @@ class ExecutionManager:
|
||||
AutomationTypes.push_account: PushAccountManager,
|
||||
AutomationTypes.change_secret: ChangeSecretManager,
|
||||
AutomationTypes.verify_account: VerifyAccountManager,
|
||||
AutomationTypes.remove_account: RemoveAccountManager,
|
||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
||||
# TODO 后期迁移到自动化策略中
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -10,10 +11,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: oralce
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Gather posix account
|
||||
- name: Gather windows account
|
||||
ansible.builtin.win_shell: net user
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Define info by set_fact
|
||||
ansible.builtin.set_fact:
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import GatheredAccount
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_org
|
||||
from users.models import User
|
||||
from .filter import GatherAccountsFilter
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
from ...notifications import GatherAccountChangeMsg
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -12,6 +17,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_asset_mapper = {}
|
||||
self.asset_account_info = {}
|
||||
|
||||
self.asset_username_mapper = defaultdict(set)
|
||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
||||
|
||||
@classmethod
|
||||
@@ -26,10 +34,11 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def filter_success_result(self, tp, result):
|
||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||
return result
|
||||
@staticmethod
|
||||
def generate_data(asset, result):
|
||||
|
||||
def generate_data(self, asset, result):
|
||||
data = []
|
||||
for username, info in result.items():
|
||||
self.asset_username_mapper[str(asset.id)].add(username)
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if info.get('date'):
|
||||
d['date_last_login'] = info['date']
|
||||
@@ -38,26 +47,85 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
data.append(d)
|
||||
return data
|
||||
|
||||
def update_or_create_accounts(self, asset, result):
|
||||
def collect_asset_account_info(self, asset, result):
|
||||
data = self.generate_data(asset, result)
|
||||
with tmp_to_org(asset.org_id):
|
||||
gathered_accounts = []
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for d in data:
|
||||
username = d['username']
|
||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
||||
defaults=d, asset=asset, username=username,
|
||||
)
|
||||
gathered_accounts.append(gathered_account)
|
||||
if not self.is_sync_account:
|
||||
return
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
self.asset_account_info[asset] = data
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
info = result.get('debug', {}).get('res', {}).get('info', {})
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
result = self.filter_success_result(asset.type, info)
|
||||
self.update_or_create_accounts(asset, result)
|
||||
self.collect_asset_account_info(asset, result)
|
||||
else:
|
||||
logger.error("Not found info".format(host))
|
||||
logger.error(f'Not found {host} info')
|
||||
|
||||
def update_or_create_accounts(self):
|
||||
for asset, data in self.asset_account_info.items():
|
||||
with tmp_to_org(asset.org_id):
|
||||
gathered_accounts = []
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for d in data:
|
||||
username = d['username']
|
||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
||||
defaults=d, asset=asset, username=username,
|
||||
)
|
||||
gathered_accounts.append(gathered_account)
|
||||
if not self.is_sync_account:
|
||||
continue
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
super().run(*args, **kwargs)
|
||||
users, change_info = self.generate_send_users_and_change_info()
|
||||
self.update_or_create_accounts()
|
||||
self.send_email_if_need(users, change_info)
|
||||
|
||||
def generate_send_users_and_change_info(self):
|
||||
recipients = self.execution.recipients
|
||||
if not self.asset_username_mapper or not recipients:
|
||||
return None, None
|
||||
|
||||
users = User.objects.filter(id__in=recipients)
|
||||
if not users:
|
||||
return users, None
|
||||
|
||||
asset_ids = self.asset_username_mapper.keys()
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
|
||||
asset_id_map = {str(asset.id): asset for asset in assets}
|
||||
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
||||
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
||||
|
||||
system_asset_username_mapper = defaultdict(set)
|
||||
for asset_id, username in asset_id_username:
|
||||
system_asset_username_mapper[str(asset_id)].add(username)
|
||||
|
||||
change_info = {}
|
||||
for asset_id, usernames in self.asset_username_mapper.items():
|
||||
system_usernames = system_asset_username_mapper.get(asset_id)
|
||||
|
||||
if not system_usernames:
|
||||
continue
|
||||
|
||||
add_usernames = usernames - system_usernames
|
||||
remove_usernames = system_usernames - usernames
|
||||
k = f'{asset_id_map[asset_id]}[{asset_id}]'
|
||||
|
||||
if not add_usernames and not remove_usernames:
|
||||
continue
|
||||
|
||||
change_info[k] = {
|
||||
'add_usernames': ', '.join(add_usernames),
|
||||
'remove_usernames': ', '.join(remove_usernames),
|
||||
}
|
||||
|
||||
return users, change_info
|
||||
|
||||
@staticmethod
|
||||
def send_email_if_need(users, change_info):
|
||||
if not users or not change_info:
|
||||
return
|
||||
|
||||
for user in users:
|
||||
GatherAccountChangeMsg(user, change_info).publish_async()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -11,10 +12,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -28,10 +29,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -45,8 +46,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
@@ -39,3 +39,4 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
|
||||
@@ -9,7 +9,7 @@ params:
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
@@ -18,19 +18,44 @@ params:
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: '家目录'
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: '默认家目录 /home/系统用户名: /home/username'
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Aix account push:
|
||||
zh: 使用 Ansible 模块 user 执行 Aix 账号推送 (DES)
|
||||
ja: Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)
|
||||
en: Using Ansible module user to push account (DES)
|
||||
zh: '使用 Ansible 模块 user 执行 Aix 账号推送 (DES)'
|
||||
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
|
||||
en: 'Using Ansible module user to push account (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'
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ params:
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
@@ -20,18 +20,43 @@ params:
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: '家目录'
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: '默认家目录 /home/系统用户名: /home/username'
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account push:
|
||||
zh: 使用 Ansible 模块 user 执行账号推送 (sha512)
|
||||
ja: Ansible user モジュールを使用してアカウントをプッシュする (sha512)
|
||||
en: Using Ansible module user to push account (sha512)
|
||||
zh: '使用 Ansible 模块 user 执行账号推送 (sha512)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
|
||||
en: 'Using Ansible module user to push account (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'
|
||||
@@ -10,10 +10,15 @@ params:
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Windows account push:
|
||||
zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送
|
||||
ja: Ansible win_user モジュールを使用して Windows アカウントをプッシュする
|
||||
en: Using Ansible module win_user to push account
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号推送'
|
||||
ja: 'Ansible win_user モジュールを使用して Windows アカウントをプッシュする'
|
||||
en: 'Using Ansible module win_user to push account'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
- name: Push user password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
@@ -0,0 +1,25 @@
|
||||
id: push_account_windows_rdp_verify
|
||||
name: "{{ 'Windows account push rdp verify' | trans }}"
|
||||
version: 1
|
||||
method: push_account
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
priority: 49
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Windows account push rdp verify:
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号推送(最后使用 Python 模块 pyfreerdp 验证账号的可连接性)'
|
||||
ja: 'Ansible モジュール win_user を使用して Windows アカウントのプッシュを実行します (最後に Python モジュール pyfreerdp を使用してアカウントの接続性を確認します)'
|
||||
en: 'Use the Ansible module win_user to perform Windows account push (finally use the Python module pyfreerdp to verify the connectability of the account)'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
@@ -1,7 +1,4 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, Connectivity
|
||||
from assets.const import HostTypes
|
||||
from accounts.const import AutomationTypes
|
||||
from common.utils import get_logger
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
from ..change_secret.manager import ChangeSecretManager
|
||||
@@ -10,83 +7,11 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
ansible_account_prefer = ''
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.push_account
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
host = super(ChangeSecretManager, self).host_callback(
|
||||
host, asset=asset, account=account, automation=automation,
|
||||
path_dir=path_dir, **kwargs
|
||||
)
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
inventory_hosts = []
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
msg = f'Windows {asset} does not support ssh key push'
|
||||
print(msg)
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
h['name'] += '(' + account.username + ')'
|
||||
if self.secret_type is None:
|
||||
new_secret = account.secret
|
||||
else:
|
||||
new_secret = self.get_secret(secret_type)
|
||||
|
||||
self.name_recorder_mapper[h['name']] = {
|
||||
'account': account, 'new_secret': new_secret,
|
||||
}
|
||||
|
||||
private_key_path = None
|
||||
if secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
inventory_hosts.append(h)
|
||||
return inventory_hosts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
account_info = self.name_recorder_mapper.get(host)
|
||||
if not account_info:
|
||||
return
|
||||
|
||||
account = account_info['account']
|
||||
new_secret = account_info['new_secret']
|
||||
if not account:
|
||||
return
|
||||
account.secret = new_secret
|
||||
account.save(update_fields=['secret'])
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
pass
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
logger.error("Pust account error: {}".format(e))
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
return
|
||||
super(ChangeSecretManager, self).run(*args, **kwargs)
|
||||
|
||||
# @classmethod
|
||||
# def trigger_by_asset_create(cls, asset):
|
||||
# automations = PushAccountAutomation.objects.filter(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
mongodb_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
@@ -0,0 +1,12 @@
|
||||
id: remove_account_mongodb
|
||||
name: "{{ 'MongoDB account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
MongoDB account remove:
|
||||
zh: 使用 Ansible 模块 mongodb 删除账号
|
||||
ja: Ansible モジュール mongodb を使用してアカウントを削除する
|
||||
en: Delete account using Ansible module mongodb
|
||||
@@ -0,0 +1,18 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
community.mysql.mysql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
@@ -0,0 +1,14 @@
|
||||
id: remove_account_mysql
|
||||
name: "{{ 'MySQL account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
MySQL account remove:
|
||||
zh: 使用 Ansible 模块 mysql_user 删除账号
|
||||
ja: Ansible モジュール mysql_user を使用してアカウントを削除します
|
||||
en: Use the Ansible module mysql_user to delete the account
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
oracle_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ jms_account.mode }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
@@ -0,0 +1,12 @@
|
||||
id: remove_account_oracle
|
||||
name: "{{ 'Oracle account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
Oracle account remove:
|
||||
zh: 使用 Python 模块 oracledb 删除账号
|
||||
ja: Python モジュール oracledb を使用してアカウントを検証する
|
||||
en: Using Python module oracledb to verify account
|
||||
@@ -0,0 +1,15 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
community.postgresql.postgresql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
@@ -0,0 +1,12 @@
|
||||
id: remove_account_postgresql
|
||||
name: "{{ 'PostgreSQL account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
PostgreSQL account remove:
|
||||
zh: 使用 Ansible 模块 postgresql_user 删除账号
|
||||
ja: Ansible モジュール postgresql_user を使用してアカウントを削除します
|
||||
en: Use the Ansible module postgresql_user to delete the account
|
||||
@@ -0,0 +1,14 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: "{{ jms_asset.spec_info.db_name }}"
|
||||
script: "DROP USER {{ account.username }}"
|
||||
@@ -0,0 +1,12 @@
|
||||
id: remove_account_sqlserver
|
||||
name: "{{ 'SQLServer account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
SQLServer account remove:
|
||||
zh: 使用 Ansible 模块 mssql 删除账号
|
||||
ja: Ansible モジュール mssql を使用してアカウントを削除する
|
||||
en: Use Ansible module mssql to delete account
|
||||
26
apps/accounts/automations/remove_account/host/posix/main.yml
Normal file
26
apps/accounts/automations/remove_account/host/posix/main.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Get user home directory path"
|
||||
ansible.builtin.shell:
|
||||
cmd: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: user_home_dir
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if user home directory exists"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ user_home_dir.stdout }}"
|
||||
register: home_dir
|
||||
when: user_home_dir.stdout != ""
|
||||
|
||||
- name: "Rename user home directory if it exists"
|
||||
ansible.builtin.command:
|
||||
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
|
||||
when: home_dir.stat | default(false) and user_home_dir.stdout != ""
|
||||
|
||||
- name: "Remove account"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
remove: "{{ home_dir.stat.exists }}"
|
||||
when: home_dir.stat | default(false)
|
||||
@@ -0,0 +1,13 @@
|
||||
id: remove_account_posix
|
||||
name: "{{ 'Posix account remove' | trans }}"
|
||||
category: host
|
||||
type:
|
||||
- linux
|
||||
- unix
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
Posix account remove:
|
||||
zh: 使用 Ansible 模块 user 删除账号
|
||||
ja: Ansible モジュール ユーザーを使用してアカウントを削除します
|
||||
en: Use the Ansible module user to delete the account
|
||||
@@ -0,0 +1,9 @@
|
||||
- hosts: windows
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
ansible.windows.win_user:
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
purge: yes
|
||||
force: yes
|
||||
@@ -0,0 +1,13 @@
|
||||
id: remove_account_windows
|
||||
name: "{{ 'Windows account remove' | trans }}"
|
||||
version: 1
|
||||
method: remove_account
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
|
||||
i18n:
|
||||
Windows account remove:
|
||||
zh: 使用 Ansible 模块 win_user 删除账号
|
||||
ja: Ansible モジュール win_user を使用してアカウントを削除する
|
||||
en: Use the Ansible module win_user to delete an account
|
||||
67
apps/accounts/automations/remove_account/manager.py
Normal file
67
apps/accounts/automations/remove_account/manager.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import Account
|
||||
from common.utils import get_logger
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RemoveAccountManager(AccountBasePlaybookManager):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_account_mapper = {}
|
||||
|
||||
def prepare_runtime_dir(self):
|
||||
path = super().prepare_runtime_dir()
|
||||
ansible_config_path = os.path.join(path, 'ansible.cfg')
|
||||
|
||||
with open(ansible_config_path, 'w') as f:
|
||||
f.write('[ssh_connection]\n')
|
||||
f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n')
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.remove_account
|
||||
|
||||
def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet):
|
||||
gather_account_ids = self.execution.snapshot['gather_accounts']
|
||||
gather_accounts = gather_accounts.filter(id__in=gather_account_ids)
|
||||
gather_accounts = gather_accounts.exclude(
|
||||
username__in=[privilege_account.username, 'root', 'Administrator']
|
||||
)
|
||||
return gather_accounts
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
gather_accounts = asset.gatheredaccount_set.all()
|
||||
gather_accounts = self.get_gather_accounts(account, gather_accounts)
|
||||
|
||||
inventory_hosts = []
|
||||
|
||||
for gather_account in gather_accounts:
|
||||
h = deepcopy(host)
|
||||
h['name'] += '(' + gather_account.username + ')'
|
||||
self.host_account_mapper[h['name']] = (asset, gather_account)
|
||||
h['account'] = {'username': gather_account.username}
|
||||
inventory_hosts.append(h)
|
||||
return inventory_hosts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
tuple_asset_gather_account = self.host_account_mapper.get(host)
|
||||
if not tuple_asset_gather_account:
|
||||
return
|
||||
asset, gather_account = tuple_asset_gather_account
|
||||
Account.objects.filter(
|
||||
asset_id=asset.id,
|
||||
username=gather_account.username
|
||||
).delete()
|
||||
gather_account.delete()
|
||||
@@ -8,7 +8,7 @@
|
||||
- name: Verify account (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
|
||||
@@ -5,9 +5,11 @@ category:
|
||||
type:
|
||||
- windows
|
||||
method: verify_account
|
||||
protocol: rdp
|
||||
priority: 1
|
||||
|
||||
i18n:
|
||||
Windows rdp account verify:
|
||||
zh: 使用 Python 模块 pyfreerdp 验证账号
|
||||
ja: Python モジュール pyfreerdp を使用してアカウントを検証する
|
||||
en: Using Python module pyfreerdp to verify account
|
||||
zh: '使用 Python 模块 pyfreerdp 验证账号'
|
||||
ja: 'Python モジュール pyfreerdp を使用してアカウントを検証する'
|
||||
en: 'Using Python module pyfreerdp to verify account'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_shell_type: sh
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
|
||||
@@ -6,9 +6,11 @@ category:
|
||||
type:
|
||||
- all
|
||||
method: verify_account
|
||||
protocol: ssh
|
||||
priority: 50
|
||||
|
||||
i18n:
|
||||
SSH account verify:
|
||||
zh: 使用 Python 模块 paramiko 验证账号
|
||||
ja: Python モジュール paramiko を使用してアカウントを検証する
|
||||
en: Using Python module paramiko to verify account
|
||||
zh: '使用 Python 模块 paramiko 验证账号'
|
||||
ja: 'Python モジュール paramiko を使用してアカウントを検証する'
|
||||
en: 'Using Python module paramiko to verify account'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: mongdb
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
@@ -10,8 +11,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
|
||||
@@ -51,6 +51,9 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
h['name'] += '(' + account.username + ')'
|
||||
self.host_account_mapper[h['name']] = account
|
||||
secret = account.secret
|
||||
if secret is None:
|
||||
print(f'account {account.name} secret is None')
|
||||
continue
|
||||
|
||||
private_key_path = None
|
||||
if account.secret_type == SecretType.SSH_KEY:
|
||||
@@ -62,7 +65,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': account.secret_type,
|
||||
'secret': secret,
|
||||
'secret': account.escape_jinja2_syntax(secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
|
||||
__all__ = [
|
||||
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
||||
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
||||
'PushAccountActionChoice',
|
||||
'PushAccountActionChoice', 'AccountBackupType'
|
||||
]
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class AutomationTypes(models.TextChoices):
|
||||
push_account = 'push_account', _('Push account')
|
||||
change_secret = 'change_secret', _('Change secret')
|
||||
verify_account = 'verify_account', _('Verify account')
|
||||
remove_account = 'remove_account', _('Remove account')
|
||||
gather_accounts = 'gather_accounts', _('Gather accounts')
|
||||
verify_gateway_account = 'verify_gateway_account', _('Verify gateway account')
|
||||
|
||||
@@ -95,3 +96,10 @@ class TriggerChoice(models.TextChoices, TreeChoices):
|
||||
class PushAccountActionChoice(models.TextChoices):
|
||||
create_and_push = 'create_and_push', _('Create and push')
|
||||
only_create = 'only_create', _('Only create')
|
||||
|
||||
|
||||
class AccountBackupType(models.TextChoices):
|
||||
"""Backup type"""
|
||||
email = 'email', _('Email')
|
||||
# 目前只支持sftp方式
|
||||
object_storage = 'object_storage', _('SFTP')
|
||||
|
||||
@@ -51,6 +51,8 @@ class AccountFilterSet(BaseFilterSet):
|
||||
|
||||
class GatheredAccountFilterSet(BaseFilterSet):
|
||||
node_id = drf_filters.CharFilter(method='filter_nodes')
|
||||
asset_id = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
|
||||
asset_name = drf_filters.CharFilter(field_name='asset__name', lookup_expr='icontains')
|
||||
|
||||
@staticmethod
|
||||
def filter_nodes(queryset, name, value):
|
||||
@@ -58,4 +60,4 @@ class GatheredAccountFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = GatheredAccount
|
||||
fields = ['id', 'asset_id', 'username']
|
||||
fields = ['id', 'username']
|
||||
|
||||
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||
'verbose_name': 'Automation execution',
|
||||
'verbose_name_plural': 'Automation executions',
|
||||
'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
||||
('add_changesecretexection', 'Can add change secret execution'),
|
||||
('add_changesecretexecution', 'Can add change secret execution'),
|
||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||
('add_gatheraccountsexecution', 'Can add gather accounts execution')],
|
||||
'proxy': True,
|
||||
@@ -116,7 +116,7 @@ class Migration(migrations.Migration):
|
||||
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
|
||||
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
||||
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
||||
('status', models.CharField(default='pending', max_length=16)),
|
||||
('status', models.CharField(default='pending', max_length=16, verbose_name='Status')),
|
||||
('error', models.TextField(blank=True, null=True, verbose_name='Error')),
|
||||
('account',
|
||||
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),
|
||||
@@ -184,7 +184,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterModelOptions(
|
||||
name='automationexecution',
|
||||
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
||||
('add_changesecretexection', 'Can add change secret execution'),
|
||||
('add_changesecretexecution', 'Can add change secret execution'),
|
||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
||||
('view_pushaccountexecution', 'Can view push account execution'),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0006_gatheredaccount'),
|
||||
]
|
||||
@@ -12,6 +11,13 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='account',
|
||||
options={'permissions': [('view_accountsecret', 'Can view asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret'), ('verify_account', 'Can verify account'), ('push_account', 'Can push account')], 'verbose_name': 'Account'},
|
||||
options={'permissions': [
|
||||
('view_accountsecret', 'Can view asset account secret'),
|
||||
('view_historyaccount', 'Can view asset history account'),
|
||||
('view_historyaccountsecret', 'Can view asset history account secret'),
|
||||
('verify_account', 'Can verify account'),
|
||||
('push_account', 'Can push account'),
|
||||
('remove_account', 'Can remove account'),
|
||||
], 'verbose_name': 'Account'},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-24 05:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('accounts', '0016_accounttemplate_password_rules'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='automationexecution',
|
||||
options={
|
||||
'permissions': [
|
||||
('view_changesecretexecution', 'Can view change secret execution'),
|
||||
('add_changesecretexecution', 'Can add change secret execution'),
|
||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
||||
('view_pushaccountexecution', 'Can view push account execution'),
|
||||
('add_pushaccountexecution', 'Can add push account execution')
|
||||
],
|
||||
'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 4.1.10 on 2023-11-03 07:10
|
||||
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('terminal', '0067_alter_replaystorage_type'),
|
||||
('accounts', '0017_alter_automationexecution_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='accountbackupautomation',
|
||||
name='backup_type',
|
||||
field=models.CharField(choices=[('email', 'Email'), ('object_storage', 'Object Storage')], default='email', max_length=128),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accountbackupautomation',
|
||||
name='is_password_divided_by_email',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accountbackupautomation',
|
||||
name='is_password_divided_by_obj_storage',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accountbackupautomation',
|
||||
name='obj_recipients_part_one',
|
||||
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_one_plans', to='terminal.replaystorage', verbose_name='Object Storage Recipient part one'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accountbackupautomation',
|
||||
name='obj_recipients_part_two',
|
||||
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_two_plans', to='terminal.replaystorage', verbose_name='Object Storage Recipient part two'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accountbackupautomation',
|
||||
name='zip_encrypt_password',
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True, verbose_name='Zip Encrypt Password'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-31 06:12
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('accounts', '0018_accountbackupautomation_backup_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gatheraccountsautomation',
|
||||
name='recipients',
|
||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.1.10 on 2023-11-16 02:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0019_gatheraccountsautomation_recipients'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='accountbackupautomation',
|
||||
name='backup_type',
|
||||
field=models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email', max_length=128, verbose_name='Backup Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accountbackupautomation',
|
||||
name='is_password_divided_by_email',
|
||||
field=models.BooleanField(default=True, verbose_name='Is Password Divided'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accountbackupautomation',
|
||||
name='is_password_divided_by_obj_storage',
|
||||
field=models.BooleanField(default=True, verbose_name='Is Password Divided'),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,7 @@ from simple_history.models import HistoricalRecords
|
||||
|
||||
from assets.models.base import AbsConnectivity
|
||||
from common.utils import lazyproperty
|
||||
from labels.mixins import LabeledMixin
|
||||
from .base import BaseAccount
|
||||
from .mixins import VaultModelMixin
|
||||
from ..const import Source
|
||||
@@ -42,7 +43,7 @@ class AccountHistoricalRecords(HistoricalRecords):
|
||||
return super().create_history_model(model, inherited)
|
||||
|
||||
|
||||
class Account(AbsConnectivity, BaseAccount):
|
||||
class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
asset = models.ForeignKey(
|
||||
'assets.Asset', related_name='accounts',
|
||||
on_delete=models.CASCADE, verbose_name=_('Asset')
|
||||
@@ -68,10 +69,15 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
('view_historyaccountsecret', _('Can view asset history account secret')),
|
||||
('verify_account', _('Can verify account')),
|
||||
('push_account', _('Can push account')),
|
||||
('remove_account', _('Can remove account')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return '{}'.format(self.username)
|
||||
if self.asset_id:
|
||||
host = self.asset.name
|
||||
else:
|
||||
host = 'Dynamic'
|
||||
return '{}({})'.format(self.name, host)
|
||||
|
||||
@lazyproperty
|
||||
def platform(self):
|
||||
@@ -95,14 +101,13 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
""" 排除自己和以自己为 su-from 的账号 """
|
||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||
|
||||
@staticmethod
|
||||
def make_account_ansible_vars(su_from):
|
||||
def make_account_ansible_vars(self, su_from):
|
||||
var = {
|
||||
'ansible_user': su_from.username,
|
||||
}
|
||||
if not su_from.secret:
|
||||
return var
|
||||
var['ansible_password'] = su_from.secret
|
||||
var['ansible_password'] = self.escape_jinja2_syntax(su_from.secret)
|
||||
var['ansible_ssh_private_key_file'] = su_from.private_key_path
|
||||
return var
|
||||
|
||||
@@ -114,14 +119,30 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
return auth
|
||||
|
||||
auth.update(self.make_account_ansible_vars(su_from))
|
||||
become_method = 'sudo' if platform.su_method != 'su' else 'su'
|
||||
become_method = platform.su_method if platform.su_method else 'sudo'
|
||||
password = su_from.secret if become_method == 'sudo' else self.secret
|
||||
auth['ansible_become'] = True
|
||||
auth['ansible_become_method'] = become_method
|
||||
auth['ansible_become_user'] = self.username
|
||||
auth['ansible_become_password'] = password
|
||||
auth['ansible_become_password'] = self.escape_jinja2_syntax(password)
|
||||
return auth
|
||||
|
||||
@staticmethod
|
||||
def escape_jinja2_syntax(value):
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
def escape(v):
|
||||
v = v.replace('{{', '__TEMP_OPEN_BRACES__') \
|
||||
.replace('}}', '__TEMP_CLOSE_BRACES__')
|
||||
|
||||
v = v.replace('__TEMP_OPEN_BRACES__', '{{ "{{" }}') \
|
||||
.replace('__TEMP_CLOSE_BRACES__', '{{ "}}" }}')
|
||||
|
||||
return v.replace('{%', '{{ "{%" }}').replace('%}', '{{ "%}" }}')
|
||||
|
||||
return escape(value)
|
||||
|
||||
|
||||
def replace_history_model_with_mixin():
|
||||
"""
|
||||
|
||||
@@ -8,10 +8,11 @@ from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const.automation import AccountBackupType
|
||||
from common.const.choices import Trigger
|
||||
from common.db import fields
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
from common.utils import get_logger
|
||||
from common.utils import lazyproperty
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from ops.mixin import PeriodTaskModelMixin
|
||||
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
|
||||
|
||||
@@ -22,6 +23,10 @@ logger = get_logger(__file__)
|
||||
|
||||
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
types = models.JSONField(default=list)
|
||||
backup_type = models.CharField(max_length=128, choices=AccountBackupType.choices,
|
||||
default=AccountBackupType.email.value, verbose_name=_('Backup Type'))
|
||||
is_password_divided_by_email = models.BooleanField(default=True, verbose_name=_('Is Password Divided'))
|
||||
is_password_divided_by_obj_storage = models.BooleanField(default=True, verbose_name=_('Is Password Divided'))
|
||||
recipients_part_one = models.ManyToManyField(
|
||||
'users.User', related_name='recipient_part_one_plans', blank=True,
|
||||
verbose_name=_("Recipient part one")
|
||||
@@ -30,6 +35,16 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
'users.User', related_name='recipient_part_two_plans', blank=True,
|
||||
verbose_name=_("Recipient part two")
|
||||
)
|
||||
obj_recipients_part_one = models.ManyToManyField(
|
||||
'terminal.ReplayStorage', related_name='obj_recipient_part_one_plans', blank=True,
|
||||
verbose_name=_("Object Storage Recipient part one")
|
||||
)
|
||||
obj_recipients_part_two = models.ManyToManyField(
|
||||
'terminal.ReplayStorage', related_name='obj_recipient_part_two_plans', blank=True,
|
||||
verbose_name=_("Object Storage Recipient part two")
|
||||
)
|
||||
zip_encrypt_password = fields.EncryptCharField(max_length=4096, blank=True, null=True,
|
||||
verbose_name=_('Zip Encrypt Password'))
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}({self.org_id})'
|
||||
@@ -49,6 +64,7 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
|
||||
def to_attr_json(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'is_periodic': self.is_periodic,
|
||||
'interval': self.interval,
|
||||
@@ -56,6 +72,10 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
'org_id': self.org_id,
|
||||
'created_by': self.created_by,
|
||||
'types': self.types,
|
||||
'backup_type': self.backup_type,
|
||||
'is_password_divided_by_email': self.is_password_divided_by_email,
|
||||
'is_password_divided_by_obj_storage': self.is_password_divided_by_obj_storage,
|
||||
'zip_encrypt_password': self.zip_encrypt_password,
|
||||
'recipients_part_one': {
|
||||
str(user.id): (str(user), bool(user.secret_key))
|
||||
for user in self.recipients_part_one.all()
|
||||
@@ -63,7 +83,15 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
'recipients_part_two': {
|
||||
str(user.id): (str(user), bool(user.secret_key))
|
||||
for user in self.recipients_part_two.all()
|
||||
}
|
||||
},
|
||||
'obj_recipients_part_one': {
|
||||
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
|
||||
for obj_storage in self.obj_recipients_part_one.all()
|
||||
},
|
||||
'obj_recipients_part_two': {
|
||||
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
|
||||
for obj_storage in self.obj_recipients_part_two.all()
|
||||
},
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -33,7 +33,7 @@ class AutomationExecution(AssetAutomationExecution):
|
||||
verbose_name_plural = _("Automation executions")
|
||||
permissions = [
|
||||
('view_changesecretexecution', _('Can view change secret execution')),
|
||||
('add_changesecretexection', _('Can add change secret execution')),
|
||||
('add_changesecretexecution', _('Can add change secret execution')),
|
||||
|
||||
('view_gatheraccountsexecution', _('Can view gather accounts execution')),
|
||||
('add_gatheraccountsexecution', _('Can add gather accounts execution')),
|
||||
|
||||
@@ -40,7 +40,7 @@ class ChangeSecretRecord(JMSBaseModel):
|
||||
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')
|
||||
status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
|
||||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||
|
||||
class Meta:
|
||||
@@ -49,9 +49,3 @@ class ChangeSecretRecord(JMSBaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return self.account.__str__()
|
||||
|
||||
@property
|
||||
def timedelta(self):
|
||||
if self.date_started and self.date_finished:
|
||||
return self.date_finished - self.date_started
|
||||
return None
|
||||
|
||||
@@ -55,11 +55,15 @@ class GatherAccountsAutomation(AccountBaseAutomation):
|
||||
is_sync_account = models.BooleanField(
|
||||
default=False, blank=True, verbose_name=_("Is sync account")
|
||||
)
|
||||
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'is_sync_account': self.is_sync_account,
|
||||
'recipients': [
|
||||
str(recipient.id) for recipient in self.recipients.all()
|
||||
]
|
||||
})
|
||||
return attr_json
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ from django.db.models import Count, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from labels.mixins import LabeledMixin
|
||||
from .account import Account
|
||||
from .base import BaseAccount, SecretWithRandomMixin
|
||||
|
||||
__all__ = ['AccountTemplate', ]
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount, SecretWithRandomMixin):
|
||||
class AccountTemplate(LabeledMixin, BaseAccount, SecretWithRandomMixin):
|
||||
su_from = models.ForeignKey(
|
||||
'self', related_name='su_to', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.tasks import send_mail_attachment_async
|
||||
from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage
|
||||
from notifications.notifications import UserMessage
|
||||
from terminal.models.component.storage import ReplayStorage
|
||||
from users.models import User
|
||||
|
||||
|
||||
@@ -20,8 +23,8 @@ class AccountBackupExecutionTaskMsg(object):
|
||||
else:
|
||||
return _("{} - The account backup passage task has been completed: "
|
||||
"the encryption password has not been set - "
|
||||
"please go to personal information -> file encryption password "
|
||||
"to set the encryption password").format(name)
|
||||
"please go to personal information -> Basic file encryption password for preference settings"
|
||||
).format(name)
|
||||
|
||||
def publish(self, attachment_list=None):
|
||||
send_mail_attachment_async(
|
||||
@@ -29,25 +32,69 @@ class AccountBackupExecutionTaskMsg(object):
|
||||
)
|
||||
|
||||
|
||||
class AccountBackupByObjStorageExecutionTaskMsg(object):
|
||||
subject = _('Notification of account backup route task results')
|
||||
|
||||
def __init__(self, name: str, obj_storage: ReplayStorage):
|
||||
self.name = name
|
||||
self.obj_storage = obj_storage
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
name = self.name
|
||||
return _('{} - The account backup passage task has been completed.'
|
||||
' See the attachment for details').format(name)
|
||||
|
||||
def publish(self, attachment_list=None):
|
||||
upload_backup_to_obj_storage(
|
||||
self.obj_storage, attachment_list
|
||||
)
|
||||
|
||||
|
||||
class ChangeSecretExecutionTaskMsg(object):
|
||||
subject = _('Notification of implementation result of encryption change plan')
|
||||
|
||||
def __init__(self, name: str, user: User):
|
||||
def __init__(self, name: str, user: User, summary):
|
||||
self.name = name
|
||||
self.user = user
|
||||
self.summary = summary
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
name = self.name
|
||||
if self.user.secret_key:
|
||||
return _('{} - The encryption change task has been completed. '
|
||||
'See the attachment for details').format(name)
|
||||
default_message = _('{} - The encryption change task has been completed. '
|
||||
'See the attachment for details').format(name)
|
||||
|
||||
else:
|
||||
return _("{} - The encryption change task has been completed: the encryption "
|
||||
"password has not been set - please go to personal information -> "
|
||||
"file encryption password to set the encryption password").format(name)
|
||||
default_message = _("{} - The encryption change task has been completed: the encryption "
|
||||
"password has not been set - please go to personal information -> "
|
||||
"set encryption password in preferences").format(name)
|
||||
return self.summary + '\n' + default_message
|
||||
|
||||
def publish(self, attachments=None):
|
||||
send_mail_attachment_async(
|
||||
self.subject, self.message, [self.user.email], attachments
|
||||
)
|
||||
|
||||
|
||||
class GatherAccountChangeMsg(UserMessage):
|
||||
subject = _('Gather account change information')
|
||||
|
||||
def __init__(self, user, change_info: dict):
|
||||
self.change_info = change_info
|
||||
super().__init__(user)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
context = {'change_info': self.change_info}
|
||||
message = render_to_string('accounts/asset_account_change_info.html', context)
|
||||
|
||||
return {
|
||||
'subject': str(self.subject),
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
user = User.objects.first()
|
||||
return cls(user, {})
|
||||
|
||||
19
apps/accounts/permissions.py
Normal file
19
apps/accounts/permissions.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
def check_permissions(request):
|
||||
act = request.data.get('action')
|
||||
if act == 'push':
|
||||
code = 'accounts.push_account'
|
||||
elif act == 'remove':
|
||||
code = 'accounts.remove_account'
|
||||
else:
|
||||
code = 'accounts.verify_account'
|
||||
return request.user.has_perm(code)
|
||||
|
||||
|
||||
class AccountTaskActionPermission(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and check_permissions(request)
|
||||
@@ -10,7 +10,7 @@ from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from accounts.const import SecretType, Source, AccountInvalidPolicy
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from accounts.models import Account, AccountTemplate, GatheredAccount
|
||||
from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
@@ -58,7 +58,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
for data in initial_data:
|
||||
if not data.get('asset') and not self.instance:
|
||||
raise serializers.ValidationError({'asset': UniqueTogetherValidator.missing_message})
|
||||
asset = data.get('asset') or self.instance.asset
|
||||
asset = data.get('asset') or getattr(self.instance, 'asset', None)
|
||||
self.from_template_if_need(data)
|
||||
self.set_uniq_name_if_need(data, asset)
|
||||
|
||||
@@ -66,6 +66,9 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
name = initial_data.get('name')
|
||||
if name is not None:
|
||||
return
|
||||
request = self.context.get('request')
|
||||
if request and request.method == 'PATCH':
|
||||
return
|
||||
if not name:
|
||||
name = initial_data.get('username')
|
||||
if self.instance and self.instance.name == name:
|
||||
@@ -238,7 +241,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
queryset = queryset.prefetch_related(
|
||||
'asset', 'asset__platform',
|
||||
'asset__platform__automation'
|
||||
)
|
||||
).prefetch_related('labels', 'labels__label')
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -452,14 +455,20 @@ class AccountHistorySerializer(serializers.ModelSerializer):
|
||||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
('verify', 'verify'),
|
||||
('push', 'push'),
|
||||
('remove', 'remove'),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
assets = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
accounts = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Account.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
gather_accounts = serializers.PrimaryKeyRelatedField(
|
||||
queryset=GatheredAccount.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
params = serializers.JSONField(
|
||||
decoder=None, encoder=None, required=False,
|
||||
|
||||
@@ -5,7 +5,7 @@ from rest_framework import serializers
|
||||
|
||||
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
||||
from common.const.choices import Trigger
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.serializers.fields import LabeledChoiceField, EncryptedField
|
||||
from common.utils import get_logger
|
||||
from ops.mixin import PeriodTaskSerializerMixin
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
@@ -16,6 +16,11 @@ __all__ = ['AccountBackupSerializer', 'AccountBackupPlanExecutionSerializer']
|
||||
|
||||
|
||||
class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
zip_encrypt_password = EncryptedField(
|
||||
label=_('Zip Encrypt Password'), required=False, max_length=40960, allow_blank=True,
|
||||
allow_null=True, write_only=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AccountBackupAutomation
|
||||
read_only_fields = [
|
||||
@@ -24,7 +29,9 @@ class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSer
|
||||
]
|
||||
fields = read_only_fields + [
|
||||
'id', 'name', 'is_periodic', 'interval', 'crontab',
|
||||
'comment', 'types', 'recipients_part_one', 'recipients_part_two'
|
||||
'comment', 'types', 'recipients_part_one', 'recipients_part_two', 'backup_type',
|
||||
'is_password_divided_by_email', 'is_password_divided_by_obj_storage', 'obj_recipients_part_one',
|
||||
'obj_recipients_part_two', 'zip_encrypt_password'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
|
||||
@@ -5,6 +5,7 @@ from rest_framework import serializers
|
||||
from accounts.const import SecretType
|
||||
from accounts.models import BaseAccount
|
||||
from accounts.utils import validate_password_for_ansible, validate_ssh_key
|
||||
from common.serializers import ResourceLabelsMixin
|
||||
from common.serializers.fields import EncryptedField, LabeledChoiceField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
@@ -60,8 +61,7 @@ class AuthValidateMixin(serializers.Serializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
class BaseAccountSerializer(AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = BaseAccount
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
@@ -70,7 +70,7 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
'privileged', 'is_active', 'spec_info',
|
||||
]
|
||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||
fields = fields_small + fields_other
|
||||
fields = fields_small + fields_other + ['labels']
|
||||
read_only_fields = [
|
||||
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
|
||||
@@ -15,6 +15,9 @@ class PasswordRulesSerializer(serializers.Serializer):
|
||||
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
|
||||
digit = serializers.BooleanField(default=True, label=_('Digit'))
|
||||
symbol = serializers.BooleanField(default=True, label=_('Special symbol'))
|
||||
exclude_symbols = serializers.CharField(
|
||||
default='', allow_blank=True, max_length=16, label=_('Exclude symbol')
|
||||
)
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
|
||||
@@ -71,7 +71,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
return password_rules
|
||||
|
||||
length = password_rules.get('length')
|
||||
symbol_set = password_rules.get('symbol_set', '')
|
||||
|
||||
try:
|
||||
length = int(length)
|
||||
@@ -84,10 +83,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
msg = _('* Password length range 6-30 bits')
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
if not isinstance(symbol_set, str):
|
||||
symbol_set = str(symbol_set)
|
||||
|
||||
password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
|
||||
return password_rules
|
||||
|
||||
def validate(self, attrs):
|
||||
@@ -117,8 +112,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ChangeSecretRecord
|
||||
fields = [
|
||||
'id', 'asset', 'account', 'date_started', 'date_finished',
|
||||
'timedelta', 'is_success', 'error', 'execution',
|
||||
'id', 'asset', 'account', 'date_finished',
|
||||
'status', 'is_success', 'error', 'execution',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
||||
model = GatherAccountsAutomation
|
||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields \
|
||||
+ ['is_sync_account'] + read_only_fields
|
||||
+ ['is_sync_account', 'recipients'] + read_only_fields
|
||||
|
||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ def on_account_pre_save(sender, instance, **kwargs):
|
||||
if instance.version == 0:
|
||||
instance.version = 1
|
||||
else:
|
||||
instance.version = instance.history.count()
|
||||
history_account = instance.history.first()
|
||||
instance.version = history_account.version + 1 if history_account else 0
|
||||
|
||||
|
||||
@merge_delay_run(ttl=5)
|
||||
@@ -62,7 +63,7 @@ def create_accounts_activities(account, action='create'):
|
||||
def on_account_create_by_template(sender, instance, created=False, **kwargs):
|
||||
if not created or instance.source != 'template':
|
||||
return
|
||||
push_accounts_if_need(accounts=(instance,))
|
||||
push_accounts_if_need.delay(accounts=(instance,))
|
||||
create_accounts_activities(instance, action='create')
|
||||
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@ from .automation import *
|
||||
from .backup_account import *
|
||||
from .gather_accounts import *
|
||||
from .push_account import *
|
||||
from .remove_account import *
|
||||
from .template import *
|
||||
from .verify_account import *
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
|
||||
@@ -33,3 +34,39 @@ def execute_account_automation_task(pid, trigger, tp):
|
||||
return
|
||||
with tmp_to_org(instance.org):
|
||||
instance.execute(trigger)
|
||||
|
||||
|
||||
def record_task_activity_callback(self, record_id, *args, **kwargs):
|
||||
from accounts.models import ChangeSecretRecord
|
||||
with tmp_to_root_org():
|
||||
record = get_object_or_none(ChangeSecretRecord, id=record_id)
|
||||
if not record:
|
||||
return
|
||||
resource_ids = [record.id]
|
||||
org_id = record.execution.org_id
|
||||
return resource_ids, org_id
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Execute automation record'),
|
||||
activity_callback=record_task_activity_callback
|
||||
)
|
||||
def execute_automation_record_task(record_id, tp):
|
||||
from accounts.models import ChangeSecretRecord
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_none(ChangeSecretRecord, pk=record_id)
|
||||
if not instance:
|
||||
logger.error("No automation record found: {}".format(record_id))
|
||||
return
|
||||
|
||||
task_name = gettext_noop('Execute automation record')
|
||||
task_snapshot = {
|
||||
'secret': instance.new_secret,
|
||||
'secret_type': instance.execution.snapshot.get('secret_type'),
|
||||
'accounts': [str(instance.account_id)],
|
||||
'assets': [str(instance.asset_id)],
|
||||
'params': {},
|
||||
'record_id': record_id,
|
||||
}
|
||||
with tmp_to_org(instance.execution.org_id):
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
77
apps/accounts/tasks/remove_account.py
Normal file
77
apps/accounts/tasks/remove_account.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
from celery import shared_task, current_task
|
||||
from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import gettext_noop, gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import Account
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from audits.const import ActivityChoices
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
from common.utils import get_logger
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from orgs.utils import tmp_to_root_org
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['remove_accounts_task']
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Remove account'),
|
||||
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None)
|
||||
)
|
||||
def remove_accounts_task(gather_account_ids):
|
||||
from accounts.models import GatheredAccount
|
||||
|
||||
gather_accounts = GatheredAccount.objects.filter(
|
||||
id__in=gather_account_ids
|
||||
)
|
||||
task_name = gettext_noop("Remove account")
|
||||
|
||||
task_snapshot = {
|
||||
'assets': [str(i.asset_id) for i in gather_accounts],
|
||||
'gather_accounts': [str(i.id) for i in gather_accounts],
|
||||
}
|
||||
|
||||
tp = AutomationTypes.remove_account
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Clean historical accounts'))
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||
@tmp_to_root_org()
|
||||
def clean_historical_accounts():
|
||||
from audits.signal_handlers import create_activities
|
||||
print("Clean historical accounts start.")
|
||||
if settings.HISTORY_ACCOUNT_CLEAN_LIMIT >= 999:
|
||||
return
|
||||
limit = settings.HISTORY_ACCOUNT_CLEAN_LIMIT
|
||||
|
||||
history_ids_to_be_deleted = []
|
||||
history_model = Account.history.model
|
||||
history_id_mapper = defaultdict(list)
|
||||
|
||||
ids = history_model.objects.values('id').annotate(count=Count('id', distinct=True)) \
|
||||
.filter(count__gte=limit).values_list('id', flat=True)
|
||||
|
||||
if not ids:
|
||||
return
|
||||
|
||||
for i in history_model.objects.filter(id__in=ids):
|
||||
_id = str(i.id)
|
||||
history_id_mapper[_id].append(i.history_id)
|
||||
|
||||
for history_ids in history_id_mapper.values():
|
||||
history_ids_to_be_deleted.extend(history_ids[limit:])
|
||||
history_qs = history_model.objects.filter(history_id__in=history_ids_to_be_deleted)
|
||||
|
||||
resource_ids = list(history_qs.values_list('history_id', flat=True))
|
||||
history_qs.delete()
|
||||
|
||||
task_id = current_task.request.id if current_task else str(uuid.uuid4())
|
||||
detail = gettext_noop('Remove historical accounts that are out of range.')
|
||||
create_activities(resource_ids, detail, task_id, action=ActivityChoices.task, org_id='')
|
||||
@@ -0,0 +1,20 @@
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
||||
<caption></caption>
|
||||
<tr style="background-color: #f2f2f2;">
|
||||
<th style="border: 1px solid #ddd; padding: 10px; font-weight: bold;">{% trans 'Asset' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Added account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
|
||||
</tr>
|
||||
{% for name, change in change_info.items %}
|
||||
<tr style="{% cycle 'background-color: #ebf5ff;' 'background-color: #fff;' %}">
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ name }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.add_usernames }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.remove_usernames }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ class SecretGenerator:
|
||||
'lower': rules['lowercase'],
|
||||
'upper': rules['uppercase'],
|
||||
'digit': rules['digit'],
|
||||
'special_char': rules['symbol']
|
||||
'special_char': rules['symbol'],
|
||||
'exclude_chars': rules.get('exclude_symbols', ''),
|
||||
}
|
||||
return random_string(**rules)
|
||||
|
||||
@@ -46,18 +47,10 @@ class SecretGenerator:
|
||||
|
||||
def validate_password_for_ansible(password):
|
||||
""" 校验 Ansible 不支持的特殊字符 """
|
||||
# validate password contains left double curly bracket
|
||||
# check password not contains `{{`
|
||||
# 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 `'` "))
|
||||
if '"' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `"` '))
|
||||
if password.startswith('{{') and password.endswith('}}'):
|
||||
raise serializers.ValidationError(
|
||||
_('If the password starts with {{` and ends with }} `, then the password is not allowed.')
|
||||
)
|
||||
|
||||
|
||||
def validate_ssh_key(ssh_key, passphrase=None):
|
||||
|
||||
@@ -11,7 +11,7 @@ __all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet']
|
||||
class CommandGroupViewSet(OrgBulkModelViewSet):
|
||||
model = models.CommandGroup
|
||||
filterset_fields = ('name', 'command_filters')
|
||||
search_fields = filterset_fields
|
||||
search_fields = ('name',)
|
||||
serializer_class = serializers.CommandGroupSerializer
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset
|
||||
from audits.models import UserLoginLog
|
||||
from notifications.notifications import UserMessage
|
||||
@@ -16,12 +17,11 @@ class UserLoginReminderMsg(UserMessage):
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
user_log = self.user_log
|
||||
|
||||
context = {
|
||||
'ip': user_log.ip,
|
||||
'city': user_log.city,
|
||||
'username': user_log.username,
|
||||
'recipient': self.user.username,
|
||||
'recipient': self.user,
|
||||
'user_agent': user_log.user_agent,
|
||||
}
|
||||
message = render_to_string('acls/user_login_reminder.html', context)
|
||||
@@ -41,18 +41,21 @@ class UserLoginReminderMsg(UserMessage):
|
||||
class AssetLoginReminderMsg(UserMessage):
|
||||
subject = _('Asset login reminder')
|
||||
|
||||
def __init__(self, user, asset: Asset, login_user: User, account_username):
|
||||
def __init__(self, user, asset: Asset, login_user: User, account: Account, input_username):
|
||||
self.asset = asset
|
||||
self.login_user = login_user
|
||||
self.account_username = account_username
|
||||
self.account = account
|
||||
self.input_username = input_username
|
||||
super().__init__(user)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
context = {
|
||||
'recipient': self.user.username,
|
||||
'recipient': self.user,
|
||||
'username': self.login_user.username,
|
||||
'name': self.login_user.name,
|
||||
'asset': str(self.asset),
|
||||
'account': self.account_username,
|
||||
'account': self.input_username,
|
||||
'account_name': self.account.name,
|
||||
}
|
||||
message = render_to_string('acls/asset_login_reminder.html', context)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Respectful' %}{{ recipient }},</h3>
|
||||
<h3>{% trans 'Respectful' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
|
||||
<hr>
|
||||
<p><strong>{% trans 'Username' %}:</strong> [{{ username }}]</p>
|
||||
<p><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</p>
|
||||
<p><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</p>
|
||||
<p><strong>{% trans 'Account' %}:</strong> [{{ account }}]</p>
|
||||
<p><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</p>
|
||||
<hr>
|
||||
|
||||
<p>{% trans 'The user has just logged in to the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}</p>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Respectful' %}{{ recipient }},</h3>
|
||||
<h3>{% trans 'Respectful' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
|
||||
<hr>
|
||||
<p><strong>{% trans 'Username' %}:</strong> [{{ username }}]</p>
|
||||
<p><strong>{% trans 'User' %}:</strong> [{{ username }}]</p>
|
||||
<p><strong>IP:</strong> [{{ ip }}]</p>
|
||||
<p><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</p>
|
||||
<p><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</p>
|
||||
|
||||
@@ -2,8 +2,8 @@ from .asset import *
|
||||
from .category import *
|
||||
from .domain import *
|
||||
from .favorite_asset import *
|
||||
from .label import *
|
||||
from .mixin import *
|
||||
from .node import *
|
||||
from .platform import *
|
||||
from .protocol import *
|
||||
from .tree import *
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import status
|
||||
@@ -14,7 +13,7 @@ from rest_framework.status import HTTP_200_OK
|
||||
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
|
||||
from assets import serializers
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
|
||||
from assets.filters import IpInFilterBackend, NodeFilterBackend
|
||||
from assets.models import Asset, Gateway, Platform, Protocol
|
||||
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
||||
from common.api import SuggestionMixin
|
||||
@@ -22,7 +21,6 @@ from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend
|
||||
from common.utils import get_logger, is_uuid
|
||||
from orgs.mixins import generics
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..mixin import NodeFilterMixin
|
||||
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -33,7 +31,6 @@ __all__ = [
|
||||
|
||||
|
||||
class AssetFilterSet(BaseFilterSet):
|
||||
labels = django_filters.CharFilter(method='filter_labels')
|
||||
platform = django_filters.CharFilter(method='filter_platform')
|
||||
domain = django_filters.CharFilter(method='filter_domain')
|
||||
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
@@ -64,7 +61,7 @@ class AssetFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
"id", "name", "address", "is_active", "labels",
|
||||
"id", "name", "address", "is_active",
|
||||
"type", "category", "platform",
|
||||
]
|
||||
|
||||
@@ -87,25 +84,16 @@ class AssetFilterSet(BaseFilterSet):
|
||||
value = value.split(',')
|
||||
return queryset.filter(protocols__name__in=value).distinct()
|
||||
|
||||
@staticmethod
|
||||
def filter_labels(queryset, name, value):
|
||||
if ':' in value:
|
||||
n, v = value.split(':', 1)
|
||||
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).distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
model = Asset
|
||||
filterset_class = AssetFilterSet
|
||||
search_fields = ("name", "address", "comment")
|
||||
ordering_fields = ('name', 'connectivity', 'platform', 'date_updated')
|
||||
ordering = ('name',)
|
||||
ordering_fields = ('name', 'address', 'connectivity', 'platform', 'date_updated', 'date_created')
|
||||
serializer_classes = (
|
||||
("default", serializers.AssetSerializer),
|
||||
("platform", serializers.PlatformSerializer),
|
||||
@@ -121,14 +109,12 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
("sync_platform_protocols", "assets.change_asset"),
|
||||
)
|
||||
extra_filter_backends = [
|
||||
LabelFilterBackend, IpInFilterBackend,
|
||||
IpInFilterBackend,
|
||||
NodeFilterBackend, AttrRulesFilterBackend
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset() \
|
||||
.prefetch_related('nodes', 'protocols') \
|
||||
.select_related('platform', 'domain')
|
||||
queryset = super().get_queryset()
|
||||
if queryset.model is not Asset:
|
||||
queryset = queryset.select_related('asset_ptr')
|
||||
return queryset
|
||||
|
||||
@@ -48,7 +48,7 @@ class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
users = User.objects.filter(
|
||||
users = User.get_queryset().filter(
|
||||
Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms)
|
||||
).distinct()
|
||||
return users
|
||||
|
||||
@@ -13,7 +13,7 @@ __all__ = ['CategoryViewSet']
|
||||
class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
|
||||
serializer_classes = {
|
||||
'default': CategorySerializer,
|
||||
'types': TypeSerializer
|
||||
'types': TypeSerializer,
|
||||
}
|
||||
permission_classes = (IsValidUser,)
|
||||
|
||||
|
||||
@@ -20,14 +20,15 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
ordering = ('name',)
|
||||
serializer_classes = {
|
||||
'default': serializers.DomainSerializer,
|
||||
'list': serializers.DomainListSerializer,
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.query_params.get('gateway'):
|
||||
return serializers.DomainWithGatewaySerializer
|
||||
return serializers.DomainSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related('assets')
|
||||
return super().get_serializer_class()
|
||||
|
||||
|
||||
class GatewayViewSet(HostViewSet):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user