mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 00:25:16 +00:00
Compare commits
322 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba7a7288a | ||
|
|
bd11018cef | ||
|
|
8a2b0a7845 | ||
|
|
b3dbdd5389 | ||
|
|
a4643fe4b4 | ||
|
|
dce9b50ac0 | ||
|
|
e2b7c902a5 | ||
|
|
4055306cf8 | ||
|
|
9e1c5bb64d | ||
|
|
32df722e9d | ||
|
|
22f6f5c34c | ||
|
|
6fb819ca53 | ||
|
|
043c4a7a0b | ||
|
|
61d4311e24 | ||
|
|
370e1628be | ||
|
|
adf5c4a7b9 | ||
|
|
9fc1ae7b6d | ||
|
|
313757dbe9 | ||
|
|
b32e352b24 | ||
|
|
b950b48112 | ||
|
|
519eb3bef2 | ||
|
|
4e55d0f1e4 | ||
|
|
2b3bb65114 | ||
|
|
b597cfcd19 | ||
|
|
33952b2333 | ||
|
|
a47a9c0345 | ||
|
|
4e0c056867 | ||
|
|
a9b5599db5 | ||
|
|
8a2eb70ad2 | ||
|
|
776234e8cc | ||
|
|
e2406955bc | ||
|
|
dba9550bc0 | ||
|
|
6ad1362a3f | ||
|
|
dfa2f7d6c9 | ||
|
|
c55e2db75e | ||
|
|
fd3a4d887e | ||
|
|
42afc1e0bf | ||
|
|
50c89431df | ||
|
|
f1f5017be3 | ||
|
|
9b85aafa52 | ||
|
|
817268d7cd | ||
|
|
d3bbfdc458 | ||
|
|
18a390d66a | ||
|
|
73b57a662e | ||
|
|
ea325f6e52 | ||
|
|
1216f15e45 | ||
|
|
cc3911d2f1 | ||
|
|
36c083f674 | ||
|
|
98c6a93658 | ||
|
|
adc607dafe | ||
|
|
1e85805ea3 | ||
|
|
957d3660ce | ||
|
|
049f6dca67 | ||
|
|
7f4377b0e8 | ||
|
|
7dfd0ee8fe | ||
|
|
41f375a4f7 | ||
|
|
a50dfe9c18 | ||
|
|
bd8a1a7d0e | ||
|
|
5546719712 | ||
|
|
068b39d922 | ||
|
|
2e1763cce7 | ||
|
|
ff9e470ce2 | ||
|
|
3080bf3647 | ||
|
|
0b04821794 | ||
|
|
296bb88834 | ||
|
|
c57cce8881 | ||
|
|
174cc16980 | ||
|
|
5b2649f775 | ||
|
|
83829df70c | ||
|
|
64641a18e6 | ||
|
|
09303ecc56 | ||
|
|
5f48e7aeb2 | ||
|
|
25dfce621b | ||
|
|
102d3b590b | ||
|
|
a45f581b0e | ||
|
|
b3991d0388 | ||
|
|
184e8b31e6 | ||
|
|
615bcadf62 | ||
|
|
7b2f813e7f | ||
|
|
81170b4b7b | ||
|
|
c4eacbabc6 | ||
|
|
ccb0509d85 | ||
|
|
886393c539 | ||
|
|
15b0ad9c12 | ||
|
|
19e2a5b9f9 | ||
|
|
0aa2c2016f | ||
|
|
935947c97a | ||
|
|
3e7e01418d | ||
|
|
7f42e59714 | ||
|
|
840e5e8863 | ||
|
|
24fb8b2a89 | ||
|
|
c1bf854824 | ||
|
|
ab23a357f7 | ||
|
|
78bf6f5817 | ||
|
|
91a26abf9e | ||
|
|
d7e7c62c7a | ||
|
|
09bdff4a67 | ||
|
|
56328e112a | ||
|
|
1d15f7125e | ||
|
|
e6b17da57d | ||
|
|
1870fc97d5 | ||
|
|
f548b4bd2b | ||
|
|
a56ac7b34e | ||
|
|
51c9a89b1f | ||
|
|
6f3ead3c42 | ||
|
|
1036d1c132 | ||
|
|
5de5fa2e96 | ||
|
|
19043d0a66 | ||
|
|
bc3e50a529 | ||
|
|
a7ab7da61c | ||
|
|
b483f78d52 | ||
|
|
88d8a3326f | ||
|
|
8f7dcd512a | ||
|
|
d795867916 | ||
|
|
4c4f544f0d | ||
|
|
8ec26dea43 | ||
|
|
799d1e4043 | ||
|
|
b03642847e | ||
|
|
a4e635bff0 | ||
|
|
83cc339d4b | ||
|
|
bb9790a50f | ||
|
|
9be3cbb936 | ||
|
|
e599bca951 | ||
|
|
501ad698b7 | ||
|
|
50e6c96358 | ||
|
|
7cf6e54f01 | ||
|
|
709e7af953 | ||
|
|
93474766f6 | ||
|
|
542eb25e7b | ||
|
|
609d2710fa | ||
|
|
d852d2f670 | ||
|
|
087a3f2914 | ||
|
|
36f113e307 | ||
|
|
23afe81ff5 | ||
|
|
dd5b2b9101 | ||
|
|
d363118911 | ||
|
|
351d4d8123 | ||
|
|
efb9f48c6f | ||
|
|
d04b90b8e8 | ||
|
|
66f57fdb27 | ||
|
|
c949589564 | ||
|
|
992708abe8 | ||
|
|
f63f8d085d | ||
|
|
3e55447327 | ||
|
|
3ac20d80d1 | ||
|
|
3e38e4fc59 | ||
|
|
1090887b2b | ||
|
|
7c55d462cd | ||
|
|
ea16088c08 | ||
|
|
4de9e608b1 | ||
|
|
dd9a55bd5f | ||
|
|
ee22006683 | ||
|
|
09d91d8bf3 | ||
|
|
46fbc19697 | ||
|
|
44a42e4739 | ||
|
|
4de2ae607d | ||
|
|
9fee82cd14 | ||
|
|
9126c7780d | ||
|
|
0842553f8a | ||
|
|
5fae919499 | ||
|
|
1243546627 | ||
|
|
a0cb16e5c4 | ||
|
|
7b8f932dcd | ||
|
|
243eedc4f9 | ||
|
|
230ef2f662 | ||
|
|
42019c9e8a | ||
|
|
f6622f5e01 | ||
|
|
31f098449f | ||
|
|
0d4e346210 | ||
|
|
df193162f7 | ||
|
|
646f0a568b | ||
|
|
be5b4a5f71 | ||
|
|
e61511372c | ||
|
|
d4f3280427 | ||
|
|
083f061665 | ||
|
|
be7a93d81a | ||
|
|
156be0a64e | ||
|
|
a7fa2331bd | ||
|
|
9e0d731a0c | ||
|
|
1b184db956 | ||
|
|
4b9ed47cda | ||
|
|
f04e2fa090 | ||
|
|
83d12d02fb | ||
|
|
64257823c5 | ||
|
|
a7468a243d | ||
|
|
528e251f31 | ||
|
|
86a055638c | ||
|
|
b3f359d47b | ||
|
|
dbe969b064 | ||
|
|
b9258878fe | ||
|
|
19c2973501 | ||
|
|
e7a3c5a822 | ||
|
|
ff4748f9f4 | ||
|
|
60c19148dc | ||
|
|
7eedc0635e | ||
|
|
f5fd40978e | ||
|
|
72dd23dcce | ||
|
|
5b5c33116a | ||
|
|
7167515a53 | ||
|
|
17a01a12db | ||
|
|
3188692691 | ||
|
|
aab59403e1 | ||
|
|
7e7e24f51f | ||
|
|
428e8bf2a0 | ||
|
|
24b1c87121 | ||
|
|
cef93abb2f | ||
|
|
5c483084b7 | ||
|
|
167734ca5d | ||
|
|
1a9a5c28f5 | ||
|
|
430e20a49c | ||
|
|
3b056ff953 | ||
|
|
795d1b59e0 | ||
|
|
9d4f1a01fd | ||
|
|
332f65cf2f | ||
|
|
b79e6799c4 | ||
|
|
4eef425e2a | ||
|
|
3f4877f26b | ||
|
|
0e4d778335 | ||
|
|
52d20080ff | ||
|
|
ed8d72c06b | ||
|
|
5e9e3ec6f6 | ||
|
|
4f5f92deb8 | ||
|
|
d2a15ee702 | ||
|
|
a3a591da4b | ||
|
|
3f2925116e | ||
|
|
c3e2e536e0 | ||
|
|
8c133d5fdb | ||
|
|
89d8efe0f1 | ||
|
|
54303ea33f | ||
|
|
4dcd8dd8dd | ||
|
|
4f04a7d258 | ||
|
|
bf308e24b6 | ||
|
|
b3642f3ff4 | ||
|
|
3aed4955c8 | ||
|
|
9a5f9a9c92 | ||
|
|
6d5bec1ef2 | ||
|
|
e93fd1fd44 | ||
|
|
7bf37611bd | ||
|
|
b8ec4bfaa5 | ||
|
|
58b6293b76 | ||
|
|
8e12eebceb | ||
|
|
72d6ea43fa | ||
|
|
deedd49dc5 | ||
|
|
a36e6fbf84 | ||
|
|
b57453cc3c | ||
|
|
62f2909d59 | ||
|
|
0d469ff95b | ||
|
|
ca883f1fb4 | ||
|
|
6e0fbd78e7 | ||
|
|
0813cff74f | ||
|
|
ff428b84f9 | ||
|
|
d0c9aa2c55 | ||
|
|
1d5e603c0d | ||
|
|
ddbbc8df17 | ||
|
|
90df404931 | ||
|
|
b9cbff1a5f | ||
|
|
b9717eece3 | ||
|
|
f9cf2a243b | ||
|
|
e056430fce | ||
|
|
2b2821c0a1 | ||
|
|
213221beae | ||
|
|
2db9c90a74 | ||
|
|
8ced6f1168 | ||
|
|
6703ab9a77 | ||
|
|
2fc6e6cd54 | ||
|
|
2176fd8fac | ||
|
|
856e7c16e5 | ||
|
|
d4feaf1e08 | ||
|
|
5aee2ce3db | ||
|
|
4424c4bde2 | ||
|
|
5863e3e008 | ||
|
|
79a371eb6c | ||
|
|
7c7de96158 | ||
|
|
80b03e73f6 | ||
|
|
32dbab2e34 | ||
|
|
b189e363cc | ||
|
|
4c3a655239 | ||
|
|
5533114db5 | ||
|
|
4c469afa95 | ||
|
|
2ccc5beeda | ||
|
|
4b67d6925e | ||
|
|
dd979f582a | ||
|
|
042ea5e137 | ||
|
|
2a6f68c7ba | ||
|
|
43b5e97b95 | ||
|
|
619b521ea1 | ||
|
|
3447eeda68 | ||
|
|
75ef413ea5 | ||
|
|
662c9092dc | ||
|
|
c8d54b28e2 | ||
|
|
96cd307d1f | ||
|
|
6385cb3f86 | ||
|
|
36e9d8101a | ||
|
|
3354ab8ce9 | ||
|
|
89ec6ba6ef | ||
|
|
af40e46a75 | ||
|
|
86fcd3c251 | ||
|
|
c2d5928273 | ||
|
|
e656ba70ec | ||
|
|
bb807e6251 | ||
|
|
bbd6cae3d7 | ||
|
|
c3b09dd800 | ||
|
|
6d427b9834 | ||
|
|
610aaf5244 | ||
|
|
df2f1b3e6e | ||
|
|
f26b7a470a | ||
|
|
a4667f3312 | ||
|
|
91081d9423 | ||
|
|
3041697edc | ||
|
|
75d7530ea5 | ||
|
|
975cc41bce | ||
|
|
439999381d | ||
|
|
39ab5978be | ||
|
|
7be7c8cee1 | ||
|
|
68b22cbdec | ||
|
|
a7c704bea3 | ||
|
|
21993b0d89 | ||
|
|
73ccf3be5f | ||
|
|
bf3056abc4 | ||
|
|
f2fd9f5990 | ||
|
|
6d39a51c36 | ||
|
|
7fa94008c9 |
39
Dockerfile
39
Dockerfile
@@ -1,5 +1,6 @@
|
||||
FROM registry.fit2cloud.com/public/python:v3 as stage-build
|
||||
MAINTAINER Jumpserver Team <ibuler@qq.com>
|
||||
# 编译代码
|
||||
FROM python:3.8.6-slim as stage-build
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
@@ -8,33 +9,39 @@ ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
|
||||
FROM registry.fit2cloud.com/public/python:v3
|
||||
# 构建运行时环境
|
||||
FROM python:3.8.6-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
|
||||
ENV MYSQL_MIRROR=$MYSQL_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
COPY ./requirements ./requirements
|
||||
RUN useradd jumpserver
|
||||
RUN yum -y install epel-release && \
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
RUN yum -y install $(cat requirements/rpm_requirements.txt)
|
||||
RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \
|
||||
pip config set global.index-url ${PIP_MIRROR}
|
||||
RUN pip install $(grep 'jms' requirements/requirements.txt) -i https://pypi.org/simple
|
||||
RUN pip install -r requirements/requirements.txt
|
||||
COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requirements.txt
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& apt update \
|
||||
&& grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
COPY ./requirements/requirements.txt ./requirements/requirements.txt
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
RUN mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
RUN echo > config.yml
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
ENV LC_ALL=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8070
|
||||
EXPOSE 8080
|
||||
|
||||
81
README.md
81
README.md
@@ -4,9 +4,13 @@
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
|
||||
|Developer Wanted|
|
||||
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
|
||||
|安全通知|
|
||||
|------------------|
|
||||
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
|
||||
|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533), 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG|
|
||||
|
||||
--------------------------
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
@@ -16,7 +20,6 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
改变世界,从一点点开始。
|
||||
|
||||
> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
|
||||
|
||||
## 特色优势
|
||||
|
||||
@@ -28,21 +31,6 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
## 版本说明
|
||||
|
||||
自 v2.0.0 发布后, JumpServer 版本号命名将变更为:v大版本.功能版本.Bug修复版本。比如:
|
||||
|
||||
```
|
||||
v2.0.1 是 v2.0.0 之后的Bug修复版本;
|
||||
v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
```
|
||||
|
||||
像其它优秀开源项目一样,JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如:
|
||||
|
||||
```
|
||||
在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3;
|
||||
在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4;v2.1 会停止维护。
|
||||
```
|
||||
|
||||
## 功能列表
|
||||
|
||||
@@ -72,8 +60,8 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>RADIUS 二次认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>登录复核(X-PACK)</td>
|
||||
<td>用户登录行为受管理员的监管与控制</td>
|
||||
<td>登录复核</td>
|
||||
<td>用户登录行为受管理员的监管与控制:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="11">账号管理<br>Account</td>
|
||||
@@ -97,23 +85,23 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>密码过期设置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">批量改密(X-PACK)</td>
|
||||
<td>定期批量改密</td>
|
||||
<td rowspan="2">批量改密</td>
|
||||
<td>定期批量改密:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多种密码策略</td>
|
||||
<td>多种密码策略:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多云纳管(X-PACK)</td>
|
||||
<td>对私有云、公有云资产自动统一纳管</td>
|
||||
<td>多云纳管 </td>
|
||||
<td>对私有云、公有云资产自动统一纳管:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>收集用户(X-PACK)</td>
|
||||
<td>自定义任务定期收集主机用户</td>
|
||||
<td>收集用户 </td>
|
||||
<td>自定义任务定期收集主机用户:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码匣子(X-PACK)</td>
|
||||
<td>统一对资产主机的用户密码进行查看、更新、测试操作</td>
|
||||
<td>密码匣子 </td>
|
||||
<td>统一对资产主机的用户密码进行查看、更新、测试操作:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="15">授权控制<br>Authorization</td>
|
||||
@@ -138,7 +126,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>实现更细粒度的应用级授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MySQL 数据库应用、RemoteApp 远程应用(X-PACK)</td>
|
||||
<td>MySQL 数据库应用、RemoteApp 远程应用:small_orange_diamond: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>动作授权</td>
|
||||
@@ -165,12 +153,12 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>实现 Web SFTP 文件管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>工单管理(X-PACK)</td>
|
||||
<td>支持对用户登录请求行为进行控制</td>
|
||||
<td>工单管理</td>
|
||||
<td>支持对用户登录请求行为进行控制:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>组织管理(X-PACK)</td>
|
||||
<td>实现多租户管理与权限隔离</td>
|
||||
<td>组织管理</td>
|
||||
<td>实现多租户管理与权限隔离:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="7">安全审计<br>Audit</td>
|
||||
@@ -189,7 +177,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>支持对 Linux、Windows 等资产操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持对 RemoteApp(X-PACK)、MySQL 等应用操作的录像进行回放审计</td>
|
||||
<td>支持对 RemoteApp:small_orange_diamond:、MySQL 等应用操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>指令审计</td>
|
||||
@@ -205,7 +193,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>命令方式</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web UI方式 (X-PACK)</td>
|
||||
<td>Web UI方式 :small_orange_diamond:</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -213,13 +201,13 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
<td>MySQL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oracle (X-PACK)</td>
|
||||
<td>Oracle :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MariaDB (X-PACK)</td>
|
||||
<td>MariaDB :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL (X-PACK)</td>
|
||||
<td>PostgreSQL :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="6">功能亮点</td>
|
||||
@@ -249,6 +237,8 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**说明**: 带 :small_orange_diamond: 后缀的是 X-PACK 插件有的功能
|
||||
|
||||
## 快速开始
|
||||
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
@@ -258,9 +248,19 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
## 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [Koko](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## 贡献
|
||||
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
|
||||
|
||||
感谢以下贡献者,让 JumpServer 更加完善
|
||||
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
|
||||
</a>
|
||||
|
||||
|
||||
## 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化连接依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
@@ -268,7 +268,6 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
|
||||
## JumpServer 企业版
|
||||
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
|
||||
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
|
||||
|
||||
## 案例研究
|
||||
|
||||
|
||||
133
README_EN.md
133
README_EN.md
@@ -1,22 +1,135 @@
|
||||
## Jumpserver
|
||||
|
||||

|
||||

|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](http://www.paramiko.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
|
||||
----
|
||||
## CRITICAL BUG WARNING
|
||||
|
||||
Recently we have found a critical bug for remote execution vulnerability which leads to pre-auth and info leak, please fix it as soon as possible.
|
||||
|
||||
Thanks for **reactivity from Alibaba Hackerone bug bounty program** report us this bug
|
||||
|
||||
**Vulnerable version:**
|
||||
```
|
||||
< v2.6.2
|
||||
< v2.5.4
|
||||
< v2.4.5
|
||||
= v1.5.9
|
||||
>= v1.5.3
|
||||
```
|
||||
|
||||
**Safe and Stable version:**
|
||||
```
|
||||
>= v2.6.2
|
||||
>= v2.5.4
|
||||
>= v2.4.5
|
||||
= v1.5.9 (version tag didn't change)
|
||||
< v1.5.3
|
||||
```
|
||||
|
||||
**Bug Fix Solution:**
|
||||
Upgrade to the latest version or the version mentioned above
|
||||
|
||||
|
||||
**Temporary Solution (upgrade asap):**
|
||||
|
||||
Modify the Nginx config file and disable the vulnerable api listed below
|
||||
|
||||
```
|
||||
/api/v1/authentication/connection-token/
|
||||
/api/v1/users/connection-token/
|
||||
```
|
||||
|
||||
Path to Nginx config file
|
||||
|
||||
```
|
||||
# Previous Community version
|
||||
/etc/nginx/conf.d/jumpserver.conf
|
||||
|
||||
# Previous Enterprise version
|
||||
jumpserver-release/nginx/http_server.conf
|
||||
|
||||
# Latest version
|
||||
jumpserver-release/compose/config_static/http_server.conf
|
||||
```
|
||||
|
||||
Changes in Nginx config file
|
||||
|
||||
```
|
||||
### Put the following code on top of location server, or before /api and /
|
||||
location /api/v1/authentication/connection-token/ {
|
||||
return 403;
|
||||
}
|
||||
|
||||
location /api/v1/users/connection-token/ {
|
||||
return 403;
|
||||
}
|
||||
### End right here
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://core:8080;
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Save the file and restart Nginx
|
||||
|
||||
```
|
||||
docker deployment:
|
||||
$ docker restart jms_nginx
|
||||
|
||||
rpm or other deployment:
|
||||
$ systemctl restart nginx
|
||||
|
||||
```
|
||||
|
||||
**Bug Fix Verification**
|
||||
|
||||
```
|
||||
# Download the following script to check if it is fixed
|
||||
$ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh
|
||||
|
||||
# Run the code to verify it
|
||||
$ bash jms_bug_check.sh demo.jumpserver.org
|
||||
漏洞已修复 (It means the bug is fixed)
|
||||
漏洞未修复 (It means the bug is not fixed and the system is still vulnerable)
|
||||
```
|
||||
|
||||
|
||||
**Attack Simulation**
|
||||
|
||||
Go to the logs directory which should contain gunicorn.log file. Then download the "attack" script and execute it
|
||||
|
||||
```
|
||||
$ pwd
|
||||
/opt/jumpserver/core/logs
|
||||
|
||||
$ ls gunicorn.log
|
||||
gunicorn.log
|
||||
|
||||
$ wget 'https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_check_attack.sh'
|
||||
$ bash jms_check_attack.sh
|
||||
系统未被入侵 (It means the system is safe)
|
||||
系统已被入侵 (It means the system is being attacked)
|
||||
```
|
||||
|
||||
--------------------------
|
||||
|
||||
----
|
||||
|
||||
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README.md)
|
||||
|
||||
Jumpserver is the first fully open source bastion in the world, based on the GNU GPL v2.0 open source protocol. Jumpserver is a professional operation and maintenance audit system conforms to 4A specifications.
|
||||
Jumpserver is the world's first open-source PAM (Privileged Access Management System) and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
|
||||
|
||||
Jumpserver is developed using Python / Django, conforms to the Web 2.0 specification, and is equipped with the industry-leading Web Terminal solution which have beautiful interface and great user experience.
|
||||
Jumpserver uses Python / Django for development, follows Web 2.0 specifications, and is equipped with an industry-leading Web Terminal solution that provides a beautiful user interface and great user experience
|
||||
|
||||
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
|
||||
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple cross-regional areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
|
||||
|
||||
Change the world, starting from little things.
|
||||
|
||||
@@ -47,7 +160,7 @@ We provide online demo, demo video and screenshots to get you started quickly.
|
||||
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
|
||||
|
||||
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
|
||||
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK thanks to 恺珺 for provide Java SDK
|
||||
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) Thanks to 恺珺 for providing his Java SDK vesrion.
|
||||
|
||||
|
||||
### License & Copyright
|
||||
|
||||
3
apps/acls/admin.py
Normal file
3
apps/acls/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
3
apps/acls/api/__init__.py
Normal file
3
apps/acls/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .login_asset_check import *
|
||||
19
apps/acls/api/login_acl.py
Normal file
19
apps/acls/api/login_acl.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from ..models import LoginACL
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['LoginACLViewSet', ]
|
||||
|
||||
|
||||
class LoginACLViewSet(JMSBulkModelViewSet):
|
||||
queryset = LoginACL.objects.all()
|
||||
filterset_fields = ('name', 'user', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginACLSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["retrieve", "list"]:
|
||||
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
|
||||
return super().get_permissions()
|
||||
15
apps/acls/api/login_asset_acl.py
Normal file
15
apps/acls/api/login_asset_acl.py
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
__all__ = ['LoginAssetACLViewSet']
|
||||
|
||||
|
||||
class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
||||
model = models.LoginAssetACL
|
||||
filterset_fields = ('name', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginAssetACLSerializer
|
||||
105
apps/acls/api/login_asset_check.py
Normal file
105
apps/acls/api/login_asset_check.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||
|
||||
from common.permissions import IsAppUser
|
||||
from common.utils import reverse, lazyproperty
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from tickets.models import Ticket
|
||||
from ..models import LoginAssetACL
|
||||
from .. import serializers
|
||||
|
||||
|
||||
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
|
||||
|
||||
|
||||
class LoginAssetCheckAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
serializer_class = serializers.LoginAssetCheckSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
||||
return Response(data=response_data, status=200)
|
||||
|
||||
def check_if_need_confirm(self):
|
||||
queries = {
|
||||
'user': self.serializer.user, 'asset': self.serializer.asset,
|
||||
'system_user': self.serializer.system_user,
|
||||
'action': LoginAssetACL.ActionChoices.login_confirm
|
||||
}
|
||||
with tmp_to_org(self.serializer.org):
|
||||
acl = LoginAssetACL.filter(**queries).valid().first()
|
||||
|
||||
if not acl:
|
||||
is_need_confirm = False
|
||||
response_data = {}
|
||||
else:
|
||||
is_need_confirm = True
|
||||
response_data = self._get_response_data_of_need_confirm(acl)
|
||||
response_data['need_confirm'] = is_need_confirm
|
||||
return is_need_confirm, response_data
|
||||
|
||||
def _get_response_data_of_need_confirm(self, acl):
|
||||
ticket = LoginAssetACL.create_login_asset_confirm_ticket(
|
||||
user=self.serializer.user,
|
||||
asset=self.serializer.asset,
|
||||
system_user=self.serializer.system_user,
|
||||
assignees=acl.reviewers.all(),
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='acls:login-asset-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
view_name='api-tickets:ticket-detail',
|
||||
kwargs={'pk': str(ticket.id)},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
data = {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
'ticket_detail_url': ticket_detail_url,
|
||||
'reviewers': [str(user) for user in ticket.assignees.all()],
|
||||
}
|
||||
return data
|
||||
|
||||
@lazyproperty
|
||||
def serializer(self):
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if self.ticket.action_open:
|
||||
status = 'await'
|
||||
elif self.ticket.action_approve:
|
||||
status = 'approve'
|
||||
else:
|
||||
status = 'reject'
|
||||
data = {
|
||||
'status': status,
|
||||
'action': self.ticket.action,
|
||||
'processor': self.ticket.processor_display
|
||||
}
|
||||
return Response(data=data, status=200)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
if self.ticket.status_open:
|
||||
self.ticket.close(processor=self.ticket.applicant)
|
||||
data = {
|
||||
'action': self.ticket.action,
|
||||
'status': self.ticket.status,
|
||||
'processor': self.ticket.processor_display
|
||||
}
|
||||
return Response(data=data, status=200)
|
||||
|
||||
@lazyproperty
|
||||
def ticket(self):
|
||||
with tmp_to_root_org():
|
||||
return get_object_or_404(Ticket, pk=self.kwargs['pk'])
|
||||
5
apps/acls/apps.py
Normal file
5
apps/acls/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AclsConfig(AppConfig):
|
||||
name = 'acls'
|
||||
9
apps/acls/const.py
Normal file
9
apps/acls/const.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
|
||||
|
||||
ip_group_help_text = common_help_text + _(
|
||||
'Such as: '
|
||||
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
|
||||
)
|
||||
61
apps/acls/migrations/0001_initial.py
Normal file
61
apps/acls/migrations/0001_initial.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 3.1 on 2021-03-11 09:53
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LoginACL',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('ip_group', models.JSONField(default=list, verbose_name='Login IP')),
|
||||
('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', '-date_updated', 'name'),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LoginAssetACL',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('users', models.JSONField(verbose_name='User')),
|
||||
('system_users', models.JSONField(verbose_name='System User')),
|
||||
('assets', models.JSONField(verbose_name='Asset')),
|
||||
('action', models.CharField(choices=[('login_confirm', 'Login confirm')], default='login_confirm', max_length=64, verbose_name='Action')),
|
||||
('reviewers', models.ManyToManyField(blank=True, related_name='review_login_asset_acls', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', '-date_updated', 'name'),
|
||||
'unique_together': {('name', 'org_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
apps/acls/migrations/__init__.py
Normal file
0
apps/acls/migrations/__init__.py
Normal file
2
apps/acls/models/__init__.py
Normal file
2
apps/acls/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
35
apps/acls/models/base.py
Normal file
35
apps/acls/models/base.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from common.mixins import CommonModelMixin
|
||||
|
||||
|
||||
__all__ = ['BaseACL', 'BaseACLQuerySet']
|
||||
|
||||
|
||||
class BaseACLQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def inactive(self):
|
||||
return self.filter(is_active=False)
|
||||
|
||||
def valid(self):
|
||||
return self.active()
|
||||
|
||||
def invalid(self):
|
||||
return self.inactive()
|
||||
|
||||
|
||||
class BaseACL(CommonModelMixin):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
priority = models.IntegerField(
|
||||
default=50, verbose_name=_("Priority"),
|
||||
help_text=_("1-100, the lower the value will be match first"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
54
apps/acls/models/login_acl.py
Normal file
54
apps/acls/models/login_acl.py
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from ..utils import contains_ip
|
||||
|
||||
|
||||
class ACLManager(models.Manager):
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class LoginACL(BaseACL):
|
||||
class ActionChoices(models.TextChoices):
|
||||
reject = 'reject', _('Reject')
|
||||
allow = 'allow', _('Allow')
|
||||
|
||||
# 条件
|
||||
ip_group = models.JSONField(default=list, verbose_name=_('Login IP'))
|
||||
# 动作
|
||||
action = models.CharField(
|
||||
max_length=64, choices=ActionChoices.choices, default=ActionChoices.reject,
|
||||
verbose_name=_('Action')
|
||||
)
|
||||
# 关联
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User')
|
||||
)
|
||||
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
|
||||
class Meta:
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
||||
@property
|
||||
def action_allow(self):
|
||||
return self.action == self.ActionChoices.allow
|
||||
|
||||
@staticmethod
|
||||
def allow_user_to_login(user, ip):
|
||||
acl = user.login_acls.valid().first()
|
||||
if not acl:
|
||||
return True
|
||||
is_contained = contains_ip(ip, acl.ip_group)
|
||||
if acl.action_allow and is_contained:
|
||||
return True
|
||||
if acl.action_reject and not is_contained:
|
||||
return True
|
||||
return False
|
||||
99
apps/acls/models/login_asset_acl.py
Normal file
99
apps/acls/models/login_asset_acl.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from ..utils import contains_ip
|
||||
|
||||
|
||||
class ACLManager(OrgManager):
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
class ActionChoices(models.TextChoices):
|
||||
login_confirm = 'login_confirm', _('Login confirm')
|
||||
|
||||
# 条件
|
||||
users = models.JSONField(verbose_name=_('User'))
|
||||
system_users = models.JSONField(verbose_name=_('System User'))
|
||||
assets = models.JSONField(verbose_name=_('Asset'))
|
||||
# 动作
|
||||
action = models.CharField(
|
||||
max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm,
|
||||
verbose_name=_('Action')
|
||||
)
|
||||
# 动作: 附加字段
|
||||
# - login_confirm
|
||||
reviewers = models.ManyToManyField(
|
||||
'users.User', related_name='review_login_asset_acls', blank=True,
|
||||
verbose_name=_("Reviewers")
|
||||
)
|
||||
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('name', 'org_id')
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
@classmethod
|
||||
def filter(cls, user, asset, system_user, action):
|
||||
queryset = cls.objects.filter(action=action)
|
||||
queryset = cls.filter_user(user, queryset)
|
||||
queryset = cls.filter_asset(asset, queryset)
|
||||
queryset = cls.filter_system_user(system_user, queryset)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_user(cls, user, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(users__username_group__contains=user.username) |
|
||||
Q(users__username_group__contains='*')
|
||||
)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_asset(cls, asset, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(assets__hostname_group__contains=asset.hostname) |
|
||||
Q(assets__hostname_group__contains='*')
|
||||
)
|
||||
ids = [q.id for q in queryset if contains_ip(asset.ip, q.assets.get('ip_group', []))]
|
||||
queryset = cls.objects.filter(id__in=ids)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_system_user(cls, system_user, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(system_users__name_group__contains=system_user.name) |
|
||||
Q(system_users__name_group__contains='*')
|
||||
).filter(
|
||||
Q(system_users__username_group__contains=system_user.username) |
|
||||
Q(system_users__username_group__contains='*')
|
||||
).filter(
|
||||
Q(system_users__protocol_group__contains=system_user.protocol) |
|
||||
Q(system_users__protocol_group__contains='*')
|
||||
)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
|
||||
from tickets.const import TicketTypeChoices
|
||||
from tickets.models import Ticket
|
||||
data = {
|
||||
'title': _('Login asset confirm') + ' ({})'.format(user),
|
||||
'type': TicketTypeChoices.login_asset_confirm,
|
||||
'meta': {
|
||||
'apply_login_user': str(user),
|
||||
'apply_login_asset': str(asset),
|
||||
'apply_login_system_user': str(system_user),
|
||||
},
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.assignees.set(assignees)
|
||||
ticket.open(applicant=user)
|
||||
return ticket
|
||||
|
||||
3
apps/acls/serializers/__init__.py
Normal file
3
apps/acls/serializers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .login_asset_check import *
|
||||
49
apps/acls/serializers/login_acl.py
Normal file
49
apps/acls/serializers/login_acl.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from orgs.utils import current_org
|
||||
from ..models import LoginACL
|
||||
from ..utils import is_ip_address, is_ip_network, is_ip_segment
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['LoginACLSerializer', ]
|
||||
|
||||
|
||||
def ip_group_child_validator(ip_group_child):
|
||||
is_valid = ip_group_child == '*' \
|
||||
or is_ip_address(ip_group_child) \
|
||||
or is_ip_network(ip_group_child) \
|
||||
or is_ip_segment(ip_group_child)
|
||||
if not is_valid:
|
||||
error = _('IP address invalid: `{}`').format(ip_group_child)
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
|
||||
class LoginACLSerializer(BulkModelSerializer):
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], label=_('IP'), help_text=const.ip_group_help_text,
|
||||
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
|
||||
)
|
||||
user_display = serializers.ReadOnlyField(source='user.name', label=_('User'))
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
|
||||
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user):
|
||||
if user not in current_org.get_members():
|
||||
error = _('The user `{}` is not in the current organization: `{}`').format(
|
||||
user, current_org
|
||||
)
|
||||
raise serializers.ValidationError(error)
|
||||
return user
|
||||
87
apps/acls/serializers/login_asset_acl.py
Normal file
87
apps/acls/serializers/login_asset_acl.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext as _
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.models import SystemUser
|
||||
from acls import models
|
||||
from orgs.models import Organization
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['LoginAssetACLSerializer']
|
||||
|
||||
|
||||
class LoginAssetACLUsersSerializer(serializers.Serializer):
|
||||
username_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetACLAssestsSerializer(serializers.Serializer):
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'),
|
||||
help_text=const.ip_group_help_text + _('(Domain name support)')
|
||||
)
|
||||
hostname_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
||||
name_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
username_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
protocol_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||
help_text=const.common_help_text + _('Protocol options: {}').format(
|
||||
', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS)
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_protocol_group(protocol_group):
|
||||
unsupported_protocols = set(protocol_group) - set(SystemUser.ASSET_CATEGORY_PROTOCOLS + ['*'])
|
||||
if unsupported_protocols:
|
||||
error = _('Unsupported protocols: {}').format(unsupported_protocols)
|
||||
raise serializers.ValidationError(error)
|
||||
return protocol_group
|
||||
|
||||
|
||||
class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
|
||||
users = LoginAssetACLUsersSerializer()
|
||||
assets = LoginAssetACLAssestsSerializer()
|
||||
system_users = LoginAssetACLSystemUsersSerializer()
|
||||
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
|
||||
class Meta:
|
||||
model = models.LoginAssetACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display',
|
||||
'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created',
|
||||
'date_updated', 'org_id'
|
||||
]
|
||||
extra_kwargs = {
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
}
|
||||
|
||||
def validate_reviewers(self, reviewers):
|
||||
org_id = self.fields['org_id'].default()
|
||||
org = Organization.get_instance(org_id)
|
||||
if not org:
|
||||
error = _('The organization `{}` does not exist'.format(org_id))
|
||||
raise serializers.ValidationError(error)
|
||||
users = org.get_members()
|
||||
valid_reviewers = list(set(reviewers) & set(users))
|
||||
if not valid_reviewers:
|
||||
error = _('None of the reviewers belong to Organization `{}`'.format(org.name))
|
||||
raise serializers.ValidationError(error)
|
||||
return valid_reviewers
|
||||
71
apps/acls/serializers/login_asset_check.py
Normal file
71
apps/acls/serializers/login_asset_check.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.utils import get_object_or_none, lazyproperty
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser
|
||||
|
||||
|
||||
__all__ = ['LoginAssetCheckSerializer']
|
||||
|
||||
|
||||
class LoginAssetCheckSerializer(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
asset_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
system_user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
system_user_username = serializers.CharField(max_length=128, default='')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
self.asset = None
|
||||
self._system_user = None
|
||||
self._system_user_username = None
|
||||
|
||||
def validate_user_id(self, user_id):
|
||||
self.user = self.validate_object_exist(User, user_id)
|
||||
return user_id
|
||||
|
||||
def validate_asset_id(self, asset_id):
|
||||
self.asset = self.validate_object_exist(Asset, asset_id)
|
||||
return asset_id
|
||||
|
||||
def validate_system_user_id(self, system_user_id):
|
||||
self._system_user = self.validate_object_exist(SystemUser, system_user_id)
|
||||
return system_user_id
|
||||
|
||||
def validate_system_user_username(self, system_user_username):
|
||||
system_user_id = self.initial_data.get('system_user_id')
|
||||
system_user = self.validate_object_exist(SystemUser, system_user_id)
|
||||
if self._system_user.login_mode == SystemUser.LOGIN_MANUAL \
|
||||
and not system_user.username \
|
||||
and not system_user.username_same_with_user \
|
||||
and not system_user_username:
|
||||
error = 'Missing parameter: system_user_username'
|
||||
raise serializers.ValidationError(error)
|
||||
self._system_user_username = system_user_username
|
||||
return system_user_username
|
||||
|
||||
@staticmethod
|
||||
def validate_object_exist(model, field_id):
|
||||
with tmp_to_root_org():
|
||||
obj = get_object_or_none(model, pk=field_id)
|
||||
if not obj:
|
||||
error = '{} Model object does not exist'.format(model.__name__)
|
||||
raise serializers.ValidationError(error)
|
||||
return obj
|
||||
|
||||
@lazyproperty
|
||||
def system_user(self):
|
||||
if self._system_user.username_same_with_user:
|
||||
username = self.user.username
|
||||
elif self._system_user.login_mode == SystemUser.LOGIN_MANUAL:
|
||||
username = self._system_user_username
|
||||
else:
|
||||
username = self._system_user.username
|
||||
self._system_user.username = username
|
||||
return self._system_user
|
||||
|
||||
@lazyproperty
|
||||
def org(self):
|
||||
return self.asset.org
|
||||
3
apps/acls/tests.py
Normal file
3
apps/acls/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
1
apps/acls/urls/__init__.py
Normal file
1
apps/acls/urls/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .api_urls import *
|
||||
18
apps/acls/urls/api_urls.py
Normal file
18
apps/acls/urls/api_urls.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.urls import path
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
from .. import api
|
||||
|
||||
|
||||
app_name = 'acls'
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'login-acls', api.LoginACLViewSet, 'login-acl')
|
||||
router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl')
|
||||
|
||||
urlpatterns = [
|
||||
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
|
||||
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
68
apps/acls/utils.py
Normal file
68
apps/acls/utils.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from ipaddress import ip_network, ip_address
|
||||
|
||||
|
||||
def is_ip_address(address):
|
||||
""" 192.168.10.1 """
|
||||
try:
|
||||
ip_address(address)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def is_ip_network(ip):
|
||||
""" 192.168.1.0/24 """
|
||||
try:
|
||||
ip_network(ip)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def is_ip_segment(ip):
|
||||
""" 10.1.1.1-10.1.1.20 """
|
||||
if '-' not in ip:
|
||||
return False
|
||||
ip_address1, ip_address2 = ip.split('-')
|
||||
return is_ip_address(ip_address1) and is_ip_address(ip_address2)
|
||||
|
||||
|
||||
def in_ip_segment(ip, ip_segment):
|
||||
ip1, ip2 = ip_segment.split('-')
|
||||
ip1 = int(ip_address(ip1))
|
||||
ip2 = int(ip_address(ip2))
|
||||
ip = int(ip_address(ip))
|
||||
return min(ip1, ip2) <= ip <= max(ip1, ip2)
|
||||
|
||||
|
||||
def contains_ip(ip, ip_group):
|
||||
"""
|
||||
ip_group:
|
||||
[192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64.]
|
||||
|
||||
"""
|
||||
|
||||
if '*' in ip_group:
|
||||
return True
|
||||
|
||||
for _ip in ip_group:
|
||||
if is_ip_address(_ip):
|
||||
# 192.168.10.1
|
||||
if ip == _ip:
|
||||
return True
|
||||
elif is_ip_network(_ip) and is_ip_address(ip):
|
||||
# 192.168.1.0/24
|
||||
if ip_address(ip) in ip_network(_ip):
|
||||
return True
|
||||
elif is_ip_segment(_ip) and is_ip_address(ip):
|
||||
# 10.1.1.1-10.1.1.20
|
||||
if in_ip_segment(ip, _ip):
|
||||
return True
|
||||
else:
|
||||
# is domain name
|
||||
if ip == _ip:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,5 +1,3 @@
|
||||
from .application import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .mixin import ApplicationAttrsSerializerViewMixin
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import models, serializers
|
||||
|
||||
__all__ = [
|
||||
'ApplicationViewSet',
|
||||
]
|
||||
|
||||
__all__ = ['ApplicationViewSet']
|
||||
|
||||
|
||||
class ApplicationViewSet(ApplicationAttrsSerializerViewMixin, OrgBulkModelViewSet):
|
||||
class ApplicationViewSet(OrgBulkModelViewSet):
|
||||
model = models.Application
|
||||
filter_fields = ('name', 'type', 'category')
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ('name', 'type', 'category')
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.ApplicationSerializer
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'DatabaseAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.DatabaseApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DatabaseAppSerializer
|
||||
@@ -1,20 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'K8sAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSerializer
|
||||
@@ -1,48 +1,7 @@
|
||||
from common.exceptions import JMSException
|
||||
from .. import models
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
class ApplicationAttrsSerializerViewMixin:
|
||||
|
||||
def get_serializer_class(self):
|
||||
serializer_class = super().get_serializer_class()
|
||||
app_type = self.request.query_params.get('type')
|
||||
app_category = self.request.query_params.get('category')
|
||||
type_options = list(dict(models.Category.get_all_type_serializer_mapper()).keys())
|
||||
category_options = list(dict(models.Category.get_category_serializer_mapper()).keys())
|
||||
|
||||
# ListAPIView 没有 action 属性
|
||||
# 不使用method属性,因为options请求时为method为post
|
||||
action = getattr(self, 'action', 'list')
|
||||
|
||||
if app_type and app_type not in type_options:
|
||||
raise JMSException(
|
||||
'Invalid query parameter `type`, select from the following options: {}'
|
||||
''.format(type_options)
|
||||
)
|
||||
if app_category and app_category not in category_options:
|
||||
raise JMSException(
|
||||
'Invalid query parameter `category`, select from the following options: {}'
|
||||
''.format(category_options)
|
||||
)
|
||||
|
||||
if action in [
|
||||
'create', 'update', 'partial_update', 'bulk_update', 'partial_bulk_update'
|
||||
] and not app_type:
|
||||
# action: create / update
|
||||
raise JMSException(
|
||||
'The `{}` action must take the `type` query parameter'.format(action)
|
||||
)
|
||||
|
||||
if app_type:
|
||||
# action: create / update / list / retrieve / metadata
|
||||
attrs_cls = models.Category.get_type_serializer_cls(app_type)
|
||||
elif app_category:
|
||||
# action: list / retrieve / metadata
|
||||
attrs_cls = models.Category.get_category_serializer_cls(app_category)
|
||||
else:
|
||||
attrs_cls = models.Category.get_no_password_serializer_cls()
|
||||
return type('ApplicationDynamicSerializer', (serializer_class,), {'attrs': attrs_cls()})
|
||||
__all__ = ['SerializeApplicationToTreeNodeMixin']
|
||||
|
||||
|
||||
class SerializeApplicationToTreeNodeMixin:
|
||||
@@ -85,11 +44,46 @@ class SerializeApplicationToTreeNodeMixin:
|
||||
'meta': {'type': 'k8s_app'}
|
||||
}
|
||||
|
||||
def _serialize(self, application):
|
||||
def _serialize_application(self, application):
|
||||
method_name = f'_serialize_{application.category}'
|
||||
data = getattr(self, method_name)(application)
|
||||
data.update({
|
||||
'pId': application.org.id,
|
||||
'org_name': application.org_name
|
||||
})
|
||||
return data
|
||||
|
||||
def serialize_applications(self, applications):
|
||||
data = [self._serialize(application) for application in applications]
|
||||
data = [self._serialize_application(application) for application in applications]
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _serialize_organization(org):
|
||||
return {
|
||||
'id': org.id,
|
||||
'name': org.name,
|
||||
'title': org.name,
|
||||
'pId': '',
|
||||
'open': True,
|
||||
'isParent': True,
|
||||
'meta': {
|
||||
'type': 'node'
|
||||
}
|
||||
}
|
||||
|
||||
def serialize_organizations(self, organizations):
|
||||
data = [self._serialize_organization(org) for org in organizations]
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def filter_organizations(applications):
|
||||
organization_ids = set(applications.values_list('org_id', flat=True))
|
||||
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
|
||||
return organizations
|
||||
|
||||
def serialize_applications_with_org(self, applications):
|
||||
organizations = self.filter_organizations(applications)
|
||||
data_organizations = self.serialize_organizations(organizations)
|
||||
data_applications = self.serialize_applications(applications)
|
||||
data = data_organizations + data_applications
|
||||
return data
|
||||
|
||||
@@ -1,40 +1,19 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from common.exceptions import JMSException
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..hands import IsAppUser
|
||||
from .. import models
|
||||
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
|
||||
from ..serializers import RemoteAppConnectionInfoSerializer
|
||||
from ..permissions import IsRemoteApp
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteAppViewSet', 'RemoteAppConnectionInfoApi',
|
||||
'RemoteAppConnectionInfoApi',
|
||||
]
|
||||
|
||||
|
||||
class RemoteAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.RemoteApp
|
||||
filter_fields = ('name', 'type', 'comment')
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = RemoteAppSerializer
|
||||
|
||||
|
||||
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
|
||||
model = models.Application
|
||||
permission_classes = (IsAppUser, )
|
||||
permission_classes = (IsAppUser, IsRemoteApp)
|
||||
serializer_class = RemoteAppConnectionInfoSerializer
|
||||
|
||||
@staticmethod
|
||||
def check_category_allowed(obj):
|
||||
if not obj.category_is_remote_app:
|
||||
raise JMSException(
|
||||
'The request instance(`{}`) is not of category `remote_app`'.format(obj.category)
|
||||
)
|
||||
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
self.check_category_allowed(obj)
|
||||
return obj
|
||||
|
||||
@@ -1,64 +1,49 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
# RemoteApp
|
||||
class ApplicationCategoryChoices(TextChoices):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
|
||||
REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor'
|
||||
|
||||
REMOTE_APP_TYPE_CHROME = 'chrome'
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench'
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client'
|
||||
REMOTE_APP_TYPE_CUSTOM = 'custom'
|
||||
|
||||
# Fields attribute write_only default => False
|
||||
|
||||
REMOTE_APP_TYPE_CHROME_FIELDS = [
|
||||
{'name': 'chrome_target'},
|
||||
{'name': 'chrome_username'},
|
||||
{'name': 'chrome_password', 'write_only': True}
|
||||
]
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
|
||||
{'name': 'mysql_workbench_ip'},
|
||||
{'name': 'mysql_workbench_name'},
|
||||
{'name': 'mysql_workbench_port'},
|
||||
{'name': 'mysql_workbench_username'},
|
||||
{'name': 'mysql_workbench_password', 'write_only': True}
|
||||
]
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS = [
|
||||
{'name': 'vmware_target'},
|
||||
{'name': 'vmware_username'},
|
||||
{'name': 'vmware_password', 'write_only': True}
|
||||
]
|
||||
REMOTE_APP_TYPE_CUSTOM_FIELDS = [
|
||||
{'name': 'custom_cmdline'},
|
||||
{'name': 'custom_target'},
|
||||
{'name': 'custom_username'},
|
||||
{'name': 'custom_password', 'write_only': True}
|
||||
]
|
||||
|
||||
REMOTE_APP_TYPE_FIELDS_MAP = {
|
||||
REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS,
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS,
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS,
|
||||
REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS
|
||||
}
|
||||
|
||||
REMOTE_APP_TYPE_CHOICES = (
|
||||
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
|
||||
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
|
||||
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
|
||||
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
|
||||
)
|
||||
@classmethod
|
||||
def get_label(cls, category):
|
||||
return dict(cls.choices).get(category, '')
|
||||
|
||||
|
||||
# DatabaseApp
|
||||
class ApplicationTypeChoices(TextChoices):
|
||||
# db category
|
||||
mysql = 'mysql', 'MySQL'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
pgsql = 'postgresql', 'PostgreSQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
|
||||
# remote-app category
|
||||
chrome = 'chrome', 'Chrome'
|
||||
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
|
||||
vmware_client = 'vmware_client', 'vSphere Client'
|
||||
custom = 'custom', _('Custom')
|
||||
|
||||
DATABASE_APP_TYPE_MYSQL = 'mysql'
|
||||
# cloud category
|
||||
k8s = 'k8s', 'Kubernetes'
|
||||
|
||||
@classmethod
|
||||
def get_label(cls, tp):
|
||||
return dict(cls.choices).get(tp, '')
|
||||
|
||||
@classmethod
|
||||
def db_types(cls):
|
||||
return [cls.mysql.value, cls.oracle.value, cls.pgsql.value, cls.mariadb.value]
|
||||
|
||||
@classmethod
|
||||
def remote_app_types(cls):
|
||||
return [cls.chrome.value, cls.mysql_workbench.value, cls.vmware_client.value, cls.custom.value]
|
||||
|
||||
@classmethod
|
||||
def cloud_types(cls):
|
||||
return [cls.k8s.value]
|
||||
|
||||
DATABASE_APP_TYPE_CHOICES = (
|
||||
(DATABASE_APP_TYPE_MYSQL, 'MySQL'),
|
||||
)
|
||||
|
||||
18
apps/applications/migrations/0007_auto_20201119_1110.py
Normal file
18
apps/applications/migrations/0007_auto_20201119_1110.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-11-19 03:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0006_application'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='attrs',
|
||||
field=models.JSONField(),
|
||||
),
|
||||
]
|
||||
28
apps/applications/migrations/0008_auto_20210104_0435.py
Normal file
28
apps/applications/migrations/0008_auto_20210104_0435.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.1 on 2021-01-03 20:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('perms', '0017_auto_20210104_0435'),
|
||||
('applications', '0007_auto_20201119_1110'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='DatabaseApp',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='K8sApp',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='attrs',
|
||||
field=models.JSONField(default=dict, verbose_name='Attrs'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='RemoteApp',
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1 @@
|
||||
from .application import *
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
@@ -1,129 +1,25 @@
|
||||
from itertools import chain
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_mysql.models import JSONField, QuerySet
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.db.models import ChoiceSet
|
||||
|
||||
|
||||
class DBType(ChoiceSet):
|
||||
mysql = 'mysql', 'MySQL'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
pgsql = 'postgresql', 'PostgreSQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
|
||||
@classmethod
|
||||
def get_type_serializer_cls_mapper(cls):
|
||||
from ..serializers import database_app
|
||||
mapper = {
|
||||
cls.mysql: database_app.MySQLAttrsSerializer,
|
||||
cls.oracle: database_app.OracleAttrsSerializer,
|
||||
cls.pgsql: database_app.PostgreAttrsSerializer,
|
||||
cls.mariadb: database_app.MariaDBAttrsSerializer,
|
||||
}
|
||||
return mapper
|
||||
|
||||
|
||||
class RemoteAppType(ChoiceSet):
|
||||
chrome = 'chrome', 'Chrome'
|
||||
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
|
||||
vmware_client = 'vmware_client', 'vSphere Client'
|
||||
custom = 'custom', _('Custom')
|
||||
|
||||
@classmethod
|
||||
def get_type_serializer_cls_mapper(cls):
|
||||
from ..serializers import remote_app
|
||||
mapper = {
|
||||
cls.chrome: remote_app.ChromeAttrsSerializer,
|
||||
cls.mysql_workbench: remote_app.MySQLWorkbenchAttrsSerializer,
|
||||
cls.vmware_client: remote_app.VMwareClientAttrsSerializer,
|
||||
cls.custom: remote_app.CustomRemoteAppAttrsSeralizers,
|
||||
}
|
||||
return mapper
|
||||
|
||||
|
||||
class CloudType(ChoiceSet):
|
||||
k8s = 'k8s', 'Kubernetes'
|
||||
|
||||
@classmethod
|
||||
def get_type_serializer_cls_mapper(cls):
|
||||
from ..serializers import k8s_app
|
||||
mapper = {
|
||||
cls.k8s: k8s_app.K8sAttrsSerializer,
|
||||
}
|
||||
return mapper
|
||||
|
||||
|
||||
class Category(ChoiceSet):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
|
||||
@classmethod
|
||||
def get_category_type_mapper(cls):
|
||||
return {
|
||||
cls.db: DBType,
|
||||
cls.remote_app: RemoteAppType,
|
||||
cls.cloud: CloudType
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_category_type_choices_mapper(cls):
|
||||
return {
|
||||
name: tp.choices
|
||||
for name, tp in cls.get_category_type_mapper().items()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_type_choices(cls, category):
|
||||
return cls.get_category_type_choices_mapper().get(category, [])
|
||||
|
||||
@classmethod
|
||||
def get_all_type_choices(cls):
|
||||
all_grouped_choices = tuple(cls.get_category_type_choices_mapper().values())
|
||||
return tuple(chain(*all_grouped_choices))
|
||||
|
||||
@classmethod
|
||||
def get_all_type_serializer_mapper(cls):
|
||||
mapper = {}
|
||||
for tp in cls.get_category_type_mapper().values():
|
||||
mapper.update(tp.get_type_serializer_cls_mapper())
|
||||
return mapper
|
||||
|
||||
@classmethod
|
||||
def get_type_serializer_cls(cls, tp):
|
||||
mapper = cls.get_all_type_serializer_mapper()
|
||||
return mapper.get(tp, None)
|
||||
|
||||
@classmethod
|
||||
def get_category_serializer_mapper(cls):
|
||||
from ..serializers import remote_app, database_app, k8s_app
|
||||
return {
|
||||
cls.db: database_app.DBAttrsSerializer,
|
||||
cls.remote_app: remote_app.RemoteAppAttrsSerializer,
|
||||
cls.cloud: k8s_app.CloudAttrsSerializer,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_category_serializer_cls(cls, cg):
|
||||
mapper = cls.get_category_serializer_mapper()
|
||||
return mapper.get(cg, None)
|
||||
|
||||
@classmethod
|
||||
def get_no_password_serializer_cls(cls):
|
||||
from ..serializers import common
|
||||
return common.NoPasswordSerializer
|
||||
from assets.models import Asset
|
||||
from .. import const
|
||||
|
||||
|
||||
class Application(CommonModelMixin, OrgModelMixin):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
domain = models.ForeignKey('assets.Domain', null=True, blank=True, related_name='applications', verbose_name=_("Domain"), on_delete=models.SET_NULL)
|
||||
category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category'))
|
||||
type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type'))
|
||||
attrs = JSONField()
|
||||
category = models.CharField(
|
||||
max_length=16, choices=const.ApplicationCategoryChoices.choices, verbose_name=_('Category')
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=16, choices=const.ApplicationTypeChoices.choices, verbose_name=_('Type')
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
'assets.Domain', null=True, blank=True, related_name='applications',
|
||||
on_delete=models.SET_NULL, verbose_name=_("Domain"),
|
||||
)
|
||||
attrs = models.JSONField(default=dict, verbose_name=_('Attrs'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
@@ -137,5 +33,38 @@ class Application(CommonModelMixin, OrgModelMixin):
|
||||
type_display = self.get_type_display()
|
||||
return f'{self.name}({type_display})[{category_display}]'
|
||||
|
||||
def category_is_remote_app(self):
|
||||
return self.category == Category.remote_app
|
||||
@property
|
||||
def category_remote_app(self):
|
||||
return self.category == const.ApplicationCategoryChoices.remote_app.value
|
||||
|
||||
def get_rdp_remote_app_setting(self):
|
||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||
if not self.category_remote_app:
|
||||
raise ValueError(f"Not a remote app application: {self.name}")
|
||||
serializer_class = get_serializer_class_by_application_type(self.type)
|
||||
fields = serializer_class().get_fields()
|
||||
|
||||
parameters = [self.type]
|
||||
for field_name in list(fields.keys()):
|
||||
if field_name in ['asset']:
|
||||
continue
|
||||
value = self.attrs.get(field_name)
|
||||
if not value:
|
||||
continue
|
||||
if field_name == 'path':
|
||||
value = '\"%s\"' % value
|
||||
parameters.append(str(value))
|
||||
|
||||
parameters = ' '.join(parameters)
|
||||
return {
|
||||
'program': '||jmservisor',
|
||||
'working_directory': '',
|
||||
'parameters': parameters
|
||||
}
|
||||
|
||||
def get_remote_app_asset(self):
|
||||
asset_id = self.attrs.get('asset')
|
||||
if not asset_id:
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
return asset
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['DatabaseApp']
|
||||
|
||||
|
||||
class DatabaseApp(CommonModelMixin, OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=const.DATABASE_APP_TYPE_MYSQL,
|
||||
choices=const.DATABASE_APP_TYPE_CHOICES,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
host = models.CharField(
|
||||
max_length=128, verbose_name=_('Host'), db_index=True
|
||||
)
|
||||
port = models.IntegerField(default=3306, verbose_name=_('Port'))
|
||||
database = models.CharField(
|
||||
max_length=128, blank=True, null=True, verbose_name=_('Database'),
|
||||
db_index=True
|
||||
)
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _("DatabaseApp")
|
||||
ordering = ('name', )
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db import models
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
class K8sApp(OrgModelMixin, models.JMSModel):
|
||||
class TYPE(models.ChoiceSet):
|
||||
K8S = 'k8s', _('Kubernetes')
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=TYPE.K8S, choices=TYPE.choices,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
cluster = models.CharField(max_length=1024, verbose_name=_('Cluster'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _('KubernetesApp')
|
||||
ordering = ('name', )
|
||||
@@ -1,78 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.fields.model import EncryptJsonDictTextField
|
||||
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteApp',
|
||||
]
|
||||
|
||||
|
||||
class RemoteApp(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
asset = models.ForeignKey(
|
||||
'assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')
|
||||
)
|
||||
type = models.CharField(
|
||||
default=const.REMOTE_APP_TYPE_CHROME,
|
||||
choices=const.REMOTE_APP_TYPE_CHOICES,
|
||||
max_length=128, verbose_name=_('App type')
|
||||
)
|
||||
path = models.CharField(
|
||||
max_length=128, blank=False, null=False,
|
||||
verbose_name=_('App path')
|
||||
)
|
||||
params = EncryptJsonDictTextField(
|
||||
max_length=4096, default={}, blank=True, null=True,
|
||||
verbose_name=_('Parameters')
|
||||
)
|
||||
created_by = models.CharField(
|
||||
max_length=32, null=True, blank=True, verbose_name=_('Created by')
|
||||
)
|
||||
date_created = models.DateTimeField(
|
||||
auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')
|
||||
)
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("RemoteApp")
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name', )
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
"""
|
||||
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
|
||||
"""
|
||||
_parameters = list()
|
||||
_parameters.append(self.type)
|
||||
path = '\"%s\"' % self.path
|
||||
_parameters.append(path)
|
||||
for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]:
|
||||
value = self.params.get(field['name'])
|
||||
if value is None:
|
||||
continue
|
||||
_parameters.append(value)
|
||||
_parameters = ' '.join(_parameters)
|
||||
return _parameters
|
||||
|
||||
@property
|
||||
def asset_info(self):
|
||||
return {
|
||||
'id': self.asset.id,
|
||||
'hostname': self.asset.hostname
|
||||
}
|
||||
9
apps/applications/permissions.py
Normal file
9
apps/applications/permissions.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
__all__ = ['IsRemoteApp']
|
||||
|
||||
|
||||
class IsRemoteApp(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return obj.category_remote_app
|
||||
@@ -1,5 +1,2 @@
|
||||
from .application import *
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
from .common import *
|
||||
|
||||
@@ -4,15 +4,46 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
|
||||
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'ApplicationSerializer',
|
||||
'ApplicationSerializer', 'ApplicationSerializerMixin',
|
||||
]
|
||||
|
||||
|
||||
class ApplicationSerializer(BulkOrgResourceModelSerializer):
|
||||
class ApplicationSerializerMixin(serializers.Serializer):
|
||||
attrs = MethodSerializer()
|
||||
|
||||
def get_attrs_serializer(self):
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
if isinstance(self.instance, models.Application):
|
||||
_type = self.instance.type
|
||||
_category = self.instance.category
|
||||
else:
|
||||
_type = self.context['request'].query_params.get('type')
|
||||
_category = self.context['request'].query_params.get('category')
|
||||
|
||||
if _type:
|
||||
serializer_class = type_serializer_classes_mapping.get(_type)
|
||||
elif _category:
|
||||
serializer_class = category_serializer_classes_mapping.get(_category)
|
||||
else:
|
||||
serializer_class = default_serializer
|
||||
|
||||
if not serializer_class:
|
||||
serializer_class = default_serializer
|
||||
|
||||
if isinstance(serializer_class, type):
|
||||
serializer = serializer_class()
|
||||
else:
|
||||
serializer = serializer_class
|
||||
return serializer
|
||||
|
||||
|
||||
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
|
||||
|
||||
@@ -26,19 +57,8 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer):
|
||||
'created_by', 'date_created', 'date_updated', 'get_type_display',
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
attrs = validated_data.pop('attrs', {})
|
||||
instance = super().create(validated_data)
|
||||
instance.attrs = attrs
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
new_attrs = validated_data.pop('attrs', {})
|
||||
instance = super().update(instance, validated_data)
|
||||
attrs = instance.attrs
|
||||
attrs.update(new_attrs)
|
||||
instance.attrs = attrs
|
||||
instance.save()
|
||||
return instance
|
||||
def validate_attrs(self, attrs):
|
||||
_attrs = self.instance.attrs if self.instance else {}
|
||||
_attrs.update(attrs)
|
||||
return _attrs
|
||||
|
||||
|
||||
1
apps/applications/serializers/attrs/__init__.py
Normal file
1
apps/applications/serializers/attrs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .attrs import *
|
||||
@@ -0,0 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .db import *
|
||||
from .cloud import *
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['CloudSerializer']
|
||||
|
||||
|
||||
class CloudSerializer(serializers.Serializer):
|
||||
cluster = serializers.CharField(max_length=1024, label=_('Cluster'), allow_null=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['DBSerializer']
|
||||
|
||||
|
||||
class DBSerializer(serializers.Serializer):
|
||||
host = serializers.CharField(max_length=128, label=_('Host'), allow_null=True)
|
||||
port = serializers.IntegerField(label=_('Port'), allow_null=True)
|
||||
database = serializers.CharField(
|
||||
max_length=128, required=True, allow_null=True, label=_('Database')
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from common.utils import get_logger, is_uuid
|
||||
from assets.models import Asset
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
__all__ = ['RemoteAppSerializer']
|
||||
|
||||
|
||||
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
instance = super().to_internal_value(data)
|
||||
return str(instance.id)
|
||||
|
||||
def to_representation(self, value):
|
||||
# value is instance.id
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(value)
|
||||
return value
|
||||
|
||||
|
||||
class RemoteAppSerializer(serializers.Serializer):
|
||||
asset_info = serializers.SerializerMethodField()
|
||||
asset = CharPrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True
|
||||
)
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), allow_null=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_asset_info(obj):
|
||||
asset_id = obj.get('asset')
|
||||
if not asset_id or is_uuid(asset_id):
|
||||
return {}
|
||||
try:
|
||||
asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname')
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.error(e)
|
||||
return {}
|
||||
if not asset:
|
||||
return {}
|
||||
asset_info = {'id': str(asset[0]), 'hostname': asset[1]}
|
||||
return asset_info
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
from .mysql import *
|
||||
from .mariadb import *
|
||||
from .oracle import *
|
||||
from .pgsql import *
|
||||
|
||||
from .chrome import *
|
||||
from .mysql_workbench import *
|
||||
from .vmware_client import *
|
||||
from .custom import *
|
||||
|
||||
from .k8s import *
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['ChromeSerializer']
|
||||
|
||||
|
||||
class ChromeSerializer(RemoteAppSerializer):
|
||||
CHROME_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
||||
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
|
||||
)
|
||||
chrome_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
|
||||
)
|
||||
chrome_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'), allow_null=True,
|
||||
)
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['CustomSerializer']
|
||||
|
||||
|
||||
class CustomSerializer(RemoteAppSerializer):
|
||||
custom_cmdline = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Operating parameter'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target url'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..application_category import CloudSerializer
|
||||
|
||||
|
||||
__all__ = ['K8SSerializer']
|
||||
|
||||
|
||||
class K8SSerializer(CloudSerializer):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
from .mysql import MySQLSerializer
|
||||
|
||||
|
||||
__all__ = ['MariaDBSerializer']
|
||||
|
||||
|
||||
class MariaDBSerializer(MySQLSerializer):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['MySQLSerializer']
|
||||
|
||||
|
||||
class MySQLSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['MySQLWorkbenchSerializer']
|
||||
|
||||
|
||||
class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
||||
MYSQL_WORKBENCH_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
|
||||
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH,
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_ip = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('IP'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_port = serializers.IntegerField(
|
||||
required=False, label=_('Port'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_name = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Database'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['OracleSerializer']
|
||||
|
||||
|
||||
class OracleSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
|
||||
|
||||
__all__ = ['PostgreSerializer']
|
||||
|
||||
|
||||
class PostgreSerializer(DBSerializer):
|
||||
port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
|
||||
__all__ = ['VMwareClientSerializer']
|
||||
|
||||
|
||||
class VMwareClientSerializer(RemoteAppSerializer):
|
||||
PATH = r'''
|
||||
C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient
|
||||
.exe
|
||||
'''
|
||||
VMWARE_CLIENT_PATH = ''.join(PATH.split())
|
||||
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH,
|
||||
allow_null=True
|
||||
)
|
||||
vmware_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Username'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
|
||||
allow_null=True
|
||||
)
|
||||
42
apps/applications/serializers/attrs/attrs.py
Normal file
42
apps/applications/serializers/attrs/attrs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from rest_framework import serializers
|
||||
from applications import const
|
||||
from . import application_category, application_type
|
||||
|
||||
|
||||
__all__ = [
|
||||
'category_serializer_classes_mapping',
|
||||
'type_serializer_classes_mapping',
|
||||
'get_serializer_class_by_application_type',
|
||||
]
|
||||
|
||||
|
||||
# define `attrs` field `category serializers mapping`
|
||||
# ---------------------------------------------------
|
||||
|
||||
category_serializer_classes_mapping = {
|
||||
const.ApplicationCategoryChoices.db.value: application_category.DBSerializer,
|
||||
const.ApplicationCategoryChoices.remote_app.value: application_category.RemoteAppSerializer,
|
||||
const.ApplicationCategoryChoices.cloud.value: application_category.CloudSerializer,
|
||||
}
|
||||
|
||||
# define `attrs` field `type serializers mapping`
|
||||
# -----------------------------------------------
|
||||
|
||||
type_serializer_classes_mapping = {
|
||||
# db
|
||||
const.ApplicationTypeChoices.mysql.value: application_type.MySQLSerializer,
|
||||
const.ApplicationTypeChoices.mariadb.value: application_type.MariaDBSerializer,
|
||||
const.ApplicationTypeChoices.oracle.value: application_type.OracleSerializer,
|
||||
const.ApplicationTypeChoices.pgsql.value: application_type.PostgreSerializer,
|
||||
# remote-app
|
||||
const.ApplicationTypeChoices.chrome.value: application_type.ChromeSerializer,
|
||||
const.ApplicationTypeChoices.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
|
||||
const.ApplicationTypeChoices.vmware_client.value: application_type.VMwareClientSerializer,
|
||||
const.ApplicationTypeChoices.custom.value: application_type.CustomSerializer,
|
||||
# cloud
|
||||
const.ApplicationTypeChoices.k8s.value: application_type.K8SSerializer
|
||||
}
|
||||
|
||||
|
||||
def get_serializer_class_by_application_type(_application_type):
|
||||
return type_serializer_classes_mapping.get(_application_type)
|
||||
@@ -1,11 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class NoPasswordSerializer(serializers.JSONField):
|
||||
def to_representation(self, value):
|
||||
new_value = {}
|
||||
for k, v in value.items():
|
||||
if 'password' not in k:
|
||||
new_value[k] = v
|
||||
return new_value
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from .. import models
|
||||
|
||||
|
||||
class DBAttrsSerializer(serializers.Serializer):
|
||||
host = serializers.CharField(max_length=128, label=_('Host'))
|
||||
port = serializers.IntegerField(label=_('Port'))
|
||||
database = serializers.CharField(
|
||||
max_length=128, required=False, allow_blank=True, allow_null=True, label=_('Database')
|
||||
)
|
||||
|
||||
|
||||
class MySQLAttrsSerializer(DBAttrsSerializer):
|
||||
port = serializers.IntegerField(default=3306, label=_('Port'))
|
||||
|
||||
|
||||
class PostgreAttrsSerializer(DBAttrsSerializer):
|
||||
port = serializers.IntegerField(default=5432, label=_('Port'))
|
||||
|
||||
|
||||
class OracleAttrsSerializer(DBAttrsSerializer):
|
||||
port = serializers.IntegerField(default=1521, label=_('Port'))
|
||||
|
||||
|
||||
class MariaDBAttrsSerializer(MySQLAttrsSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DatabaseApp
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'type', 'get_type_display', 'host', 'port',
|
||||
'database', 'comment', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'date_updated'
|
||||
'get_type_display',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'get_type_display': {'label': _('Type for display')},
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .. import models
|
||||
|
||||
|
||||
class CloudAttrsSerializer(serializers.Serializer):
|
||||
cluster = serializers.CharField(max_length=1024, label=_('Cluster'))
|
||||
|
||||
|
||||
class K8sAttrsSerializer(CloudAttrsSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class K8sAppSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.CharField(source='get_type_display', read_only=True, label=_('Type for display'))
|
||||
|
||||
class Meta:
|
||||
model = models.K8sApp
|
||||
fields = [
|
||||
'id', 'name', 'type', 'type_display', 'comment', 'created_by',
|
||||
'date_created', 'date_updated', 'cluster'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
@@ -1,89 +1,14 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import copy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.fields.serializer import CustomMetaDictField
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.models import Asset
|
||||
from ..models import Application
|
||||
|
||||
from .. import const
|
||||
from ..models import RemoteApp, Category, Application
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
instance = super().to_internal_value(data)
|
||||
return str(instance.id)
|
||||
|
||||
def to_representation(self, value):
|
||||
# value is instance.id
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(value)
|
||||
return value
|
||||
|
||||
|
||||
class RemoteAppAttrsSerializer(serializers.Serializer):
|
||||
asset_info = serializers.SerializerMethodField()
|
||||
asset = CharPrimaryKeyRelatedField(queryset=Asset.objects, required=False, label=_("Asset"))
|
||||
path = serializers.CharField(max_length=128, label=_('Application path'))
|
||||
|
||||
@staticmethod
|
||||
def get_asset_info(obj):
|
||||
asset_info = {}
|
||||
asset_id = obj.get('asset')
|
||||
if not asset_id:
|
||||
return asset_info
|
||||
try:
|
||||
asset = Asset.objects.get(id=asset_id)
|
||||
asset_info.update({
|
||||
'id': str(asset.id),
|
||||
'hostname': asset.hostname
|
||||
})
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.error(e)
|
||||
return asset_info
|
||||
|
||||
|
||||
class ChromeAttrsSerializer(RemoteAppAttrsSerializer):
|
||||
REMOTE_APP_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
||||
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
|
||||
chrome_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
|
||||
chrome_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
|
||||
chrome_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
|
||||
|
||||
|
||||
class MySQLWorkbenchAttrsSerializer(RemoteAppAttrsSerializer):
|
||||
REMOTE_APP_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
|
||||
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
|
||||
mysql_workbench_ip = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('IP'))
|
||||
mysql_workbench_port = serializers.IntegerField(required=False, label=_('Port'))
|
||||
mysql_workbench_name = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Database'))
|
||||
mysql_workbench_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
|
||||
mysql_workbench_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
|
||||
|
||||
|
||||
class VMwareClientAttrsSerializer(RemoteAppAttrsSerializer):
|
||||
REMOTE_APP_PATH = 'C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient.exe'
|
||||
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
|
||||
vmware_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
|
||||
vmware_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
|
||||
vmware_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
|
||||
|
||||
|
||||
class CustomRemoteAppAttrsSeralizers(RemoteAppAttrsSerializer):
|
||||
custom_cmdline = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Operating parameter'))
|
||||
custom_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target url'))
|
||||
custom_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
|
||||
custom_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
|
||||
__all__ = ['RemoteAppConnectionInfoSerializer']
|
||||
|
||||
|
||||
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
|
||||
@@ -97,94 +22,10 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
read_only_fields = ['parameter_remote_app']
|
||||
|
||||
@staticmethod
|
||||
def get_parameters(obj):
|
||||
"""
|
||||
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
|
||||
"""
|
||||
serializer_cls = Category.get_type_serializer_cls(obj.type)
|
||||
fields = serializer_cls().get_fields()
|
||||
fields.pop('asset', None)
|
||||
fields_name = list(fields.keys())
|
||||
attrs = obj.attrs
|
||||
_parameters = list()
|
||||
_parameters.append(obj.type)
|
||||
for field_name in list(fields_name):
|
||||
value = attrs.get(field_name, None)
|
||||
if not value:
|
||||
continue
|
||||
if field_name == 'path':
|
||||
value = '\"%s\"' % value
|
||||
_parameters.append(str(value))
|
||||
_parameters = ' '.join(_parameters)
|
||||
return _parameters
|
||||
|
||||
def get_parameter_remote_app(self, obj):
|
||||
parameters = self.get_parameters(obj)
|
||||
parameter = {
|
||||
'program': const.REMOTE_APP_BOOT_PROGRAM_NAME,
|
||||
'working_directory': '',
|
||||
'parameters': parameters,
|
||||
}
|
||||
return parameter
|
||||
|
||||
@staticmethod
|
||||
def get_asset(obj):
|
||||
return obj.attrs.get('asset')
|
||||
|
||||
|
||||
# TODO: DELETE
|
||||
class RemoteAppParamsDictField(CustomMetaDictField):
|
||||
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
|
||||
default_type = const.REMOTE_APP_TYPE_CHROME
|
||||
convert_key_remove_type_prefix = False
|
||||
convert_key_to_upper = False
|
||||
|
||||
|
||||
# TODO: DELETE
|
||||
class RemoteAppSerializer(BulkOrgResourceModelSerializer):
|
||||
params = RemoteAppParamsDictField(label=_('Parameters'))
|
||||
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
|
||||
|
||||
class Meta:
|
||||
model = RemoteApp
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display',
|
||||
'path', 'params', 'date_created', 'created_by', 'comment',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'asset_info',
|
||||
'get_type_display'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'asset_info': {'label': _('Asset info')},
|
||||
'get_type_display': {'label': _('Type for display')},
|
||||
}
|
||||
|
||||
def process_params(self, instance, validated_data):
|
||||
new_params = copy.deepcopy(validated_data.get('params', {}))
|
||||
tp = validated_data.get('type', '')
|
||||
|
||||
if tp != instance.type:
|
||||
return new_params
|
||||
|
||||
old_params = instance.params
|
||||
fields = self.type_fields_map.get(instance.type, [])
|
||||
for field in fields:
|
||||
if not field.get('write_only', False):
|
||||
continue
|
||||
field_name = field['name']
|
||||
new_value = new_params.get(field_name, '')
|
||||
old_value = old_params.get(field_name, '')
|
||||
field_value = new_value if new_value else old_value
|
||||
new_params[field_name] = field_value
|
||||
|
||||
return new_params
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
params = self.process_params(instance, validated_data)
|
||||
validated_data['params'] = params
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_parameter_remote_app(obj):
|
||||
return obj.get_rdp_remote_app_setting()
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.urls import path, re_path
|
||||
from django.urls import path
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from common import api as capi
|
||||
from .. import api
|
||||
|
||||
|
||||
app_name = 'applications'
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
|
||||
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
|
||||
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
]
|
||||
|
||||
old_version_urlpatterns = [
|
||||
re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api)
|
||||
]
|
||||
|
||||
urlpatterns += router.urls + old_version_urlpatterns
|
||||
urlpatterns += router.urls
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# coding:utf-8
|
||||
from django.urls import path
|
||||
|
||||
app_name = 'applications'
|
||||
|
||||
urlpatterns = [
|
||||
]
|
||||
@@ -29,10 +29,14 @@ class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
Admin user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = AdminUser
|
||||
filter_fields = ("name", "username")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name", "username")
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_classes = {
|
||||
'default': serializers.AdminUserSerializer,
|
||||
'retrieve': serializers.AdminUserDetailSerializer,
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
@@ -93,8 +97,8 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
class AdminUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filter_fields = ("hostname", "ip")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("hostname", "ip")
|
||||
search_fields = filterset_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
||||
@@ -12,7 +12,7 @@ from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual
|
||||
)
|
||||
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet', 'AssetPlatformRetrieveApi',
|
||||
'AssetGatewayListApi', 'AssetPlatformViewSet',
|
||||
'AssetTaskCreateApi',
|
||||
'AssetTaskCreateApi', 'AssetsTaskCreateApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -30,10 +30,15 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
model = Asset
|
||||
filter_fields = (
|
||||
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
|
||||
"is_active"
|
||||
)
|
||||
filterset_fields = {
|
||||
'hostname': ['exact'],
|
||||
'ip': ['exact'],
|
||||
'systemuser__id': ['exact'],
|
||||
'admin_user__id': ['exact'],
|
||||
'platform__base': ['exact'],
|
||||
'is_active': ['exact'],
|
||||
'protocols': ['exact', 'icontains']
|
||||
}
|
||||
search_fields = ("hostname", "ip")
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
serializer_classes = {
|
||||
@@ -74,7 +79,7 @@ class AssetPlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filter_fields = ['name', 'base']
|
||||
filterset_fields = ['name', 'base']
|
||||
search_fields = ['name']
|
||||
|
||||
def get_permissions(self):
|
||||
@@ -90,32 +95,43 @@ class AssetPlatformViewSet(ModelViewSet):
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
|
||||
class AssetTaskCreateApi(generics.CreateAPIView):
|
||||
class AssetsTaskMixin:
|
||||
def perform_assets_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
assets = data['assets']
|
||||
action = data['action']
|
||||
if action == "refresh":
|
||||
task = update_assets_hardware_info_manual.delay(assets)
|
||||
else:
|
||||
task = test_assets_connectivity_manual.delay(assets)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
self.perform_assets_task(serializer)
|
||||
|
||||
|
||||
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
instance = get_object_or_404(Asset, pk=pk)
|
||||
return instance
|
||||
def create(self, request, *args, **kwargs):
|
||||
pk = self.kwargs.get('pk')
|
||||
request.data['assets'] = [pk]
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
asset = self.get_object()
|
||||
action = serializer.validated_data["action"]
|
||||
if action == "refresh":
|
||||
task = update_asset_hardware_info_manual.delay(asset)
|
||||
else:
|
||||
task = test_asset_connectivity_manual.delay(asset)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
|
||||
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class AssetGatewayListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewayWithAuthSerializer
|
||||
model = Asset
|
||||
|
||||
def get_queryset(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
|
||||
@@ -28,7 +28,7 @@ logger = get_logger(__name__)
|
||||
class AssetUserFilterBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
kwargs = {}
|
||||
for field in view.filter_fields:
|
||||
for field in view.filterset_fields:
|
||||
value = request.GET.get(field)
|
||||
if not value:
|
||||
continue
|
||||
@@ -78,7 +78,7 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
'retrieve': serializers.AssetUserReadSerializer,
|
||||
}
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
"id", "ip", "hostname", "username",
|
||||
"asset_id", "node_id",
|
||||
"prefer", "prefer_id",
|
||||
@@ -131,7 +131,7 @@ class AssetUserTaskCreateAPI(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.AssetUserTaskSerializer
|
||||
filter_backends = AssetUserViewSet.filter_backends
|
||||
filter_fields = AssetUserViewSet.filter_fields
|
||||
filterset_fields = AssetUserViewSet.filterset_fields
|
||||
|
||||
def get_asset_users(self):
|
||||
manager = AssetUserManager()
|
||||
|
||||
@@ -14,16 +14,16 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
|
||||
|
||||
class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilter
|
||||
filter_fields = ("name",)
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterSerializer
|
||||
|
||||
|
||||
class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilterRule
|
||||
filter_fields = ("content",)
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("content",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.CommandFilterRuleSerializer
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
|
||||
|
||||
class DomainViewSet(OrgBulkModelViewSet):
|
||||
model = Domain
|
||||
filter_fields = ("name", )
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name", )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
|
||||
@@ -31,7 +31,7 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
|
||||
class GatewayViewSet(OrgBulkModelViewSet):
|
||||
model = Gateway
|
||||
filter_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
search_fields = ("domain__name", "name", "username", "ip")
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
@@ -13,7 +13,7 @@ __all__ = ['FavoriteAssetViewSet']
|
||||
class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteAssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filter_fields = ['asset']
|
||||
filterset_fields = ['asset']
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
|
||||
@@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet):
|
||||
permission_classes = [IsOrgAdmin]
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
search_fields = ['username', 'asset__ip', 'asset__hostname']
|
||||
|
||||
@@ -28,8 +28,8 @@ __all__ = ['LabelViewSet']
|
||||
|
||||
class LabelViewSet(OrgBulkModelViewSet):
|
||||
model = Label
|
||||
filter_fields = ("name", "value")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name", "value")
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LabelSerializer
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from typing import List
|
||||
|
||||
from common.utils.common import timeit
|
||||
from assets.models import Node, Asset
|
||||
from assets.pagination import AssetLimitOffsetPagination
|
||||
from common.utils import lazyproperty, dict_get_any, is_uuid, get_object_or_none
|
||||
from assets.pagination import NodeAssetTreePagination
|
||||
from common.utils import lazyproperty
|
||||
from assets.utils import get_node, is_query_node_all_assets
|
||||
|
||||
|
||||
class SerializeToTreeNodeMixin:
|
||||
permission_classes = ()
|
||||
|
||||
@timeit
|
||||
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
|
||||
if with_asset_amount:
|
||||
def _name(node: Node):
|
||||
@@ -45,6 +46,7 @@ class SerializeToTreeNodeMixin:
|
||||
return platform
|
||||
return default
|
||||
|
||||
@timeit
|
||||
def serialize_assets(self, assets, node_key=None):
|
||||
if node_key is None:
|
||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||
@@ -69,6 +71,7 @@ class SerializeToTreeNodeMixin:
|
||||
'ip': asset.ip,
|
||||
'protocols': asset.protocols_as_list,
|
||||
'platform': asset.platform_base,
|
||||
'org_name': asset.org_name
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -78,7 +81,7 @@ class SerializeToTreeNodeMixin:
|
||||
|
||||
|
||||
class FilterAssetByNodeMixin:
|
||||
pagination_class = AssetLimitOffsetPagination
|
||||
pagination_class = NodeAssetTreePagination
|
||||
|
||||
@lazyproperty
|
||||
def is_query_node_all_assets(self):
|
||||
|
||||
@@ -5,25 +5,26 @@ from collections import namedtuple, defaultdict
|
||||
from rest_framework import status
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404, Http404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.db.models.signals import m2m_changed
|
||||
|
||||
from common.const.http import POST
|
||||
from common.exceptions import SomeoneIsDoingThis
|
||||
from common.const.signals import PRE_REMOVE, POST_REMOVE
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.lock import org_level_transaction_lock
|
||||
from orgs.utils import current_org
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Node
|
||||
from ..tasks import (
|
||||
update_node_assets_hardware_info_manual,
|
||||
test_node_assets_connectivity_manual,
|
||||
check_node_assets_amount_task
|
||||
)
|
||||
from .. import serializers
|
||||
from .mixin import SerializeToTreeNodeMixin
|
||||
@@ -41,7 +42,7 @@ __all__ = [
|
||||
|
||||
class NodeViewSet(OrgModelViewSet):
|
||||
model = Node
|
||||
filter_fields = ('value', 'key', 'id')
|
||||
filterset_fields = ('value', 'key', 'id')
|
||||
search_fields = ('value', )
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
@@ -52,6 +53,11 @@ class NodeViewSet(OrgModelViewSet):
|
||||
serializer.validated_data["key"] = child_key
|
||||
serializer.save()
|
||||
|
||||
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
|
||||
def check_assets_amount_task(self, request):
|
||||
task = check_node_assets_amount_task.delay(current_org.id)
|
||||
return Response(data={'task': task.id})
|
||||
|
||||
def perform_update(self, serializer):
|
||||
node = self.get_object()
|
||||
if node.is_org_root() and node.value != serializer.validated_data['value']:
|
||||
@@ -61,6 +67,9 @@ class NodeViewSet(OrgModelViewSet):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
node = self.get_object()
|
||||
if node.is_org_root():
|
||||
error = _("You can't delete the root node ({})".format(node.value))
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
if node.has_children_or_has_assets():
|
||||
error = _("Deletion failed and the node contains children or assets")
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
@@ -118,9 +127,13 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
|
||||
key = self.request.query_params.get("key")
|
||||
|
||||
if not pk and not key:
|
||||
node = Node.org_root()
|
||||
self.is_initial = True
|
||||
if current_org.is_root():
|
||||
node = None
|
||||
else:
|
||||
node = Node.org_root()
|
||||
return node
|
||||
if pk:
|
||||
node = get_object_or_404(Node, pk=pk)
|
||||
@@ -128,16 +141,26 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
node = get_object_or_404(Node, key=key)
|
||||
return node
|
||||
|
||||
def get_org_root_queryset(self, query_all):
|
||||
if query_all:
|
||||
return Node.objects.all()
|
||||
else:
|
||||
return Node.org_root_nodes()
|
||||
|
||||
def get_queryset(self):
|
||||
query_all = self.request.query_params.get("all", "0") == "all"
|
||||
if not self.instance:
|
||||
return Node.objects.none()
|
||||
|
||||
if self.is_initial and current_org.is_root():
|
||||
return self.get_org_root_queryset(query_all)
|
||||
|
||||
if self.is_initial:
|
||||
with_self = True
|
||||
else:
|
||||
with_self = False
|
||||
|
||||
if not self.instance:
|
||||
return Node.objects.none()
|
||||
|
||||
if query_all:
|
||||
queryset = self.instance.get_all_children(with_self=with_self)
|
||||
else:
|
||||
@@ -169,12 +192,12 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
|
||||
def get_assets(self):
|
||||
include_assets = self.request.query_params.get('assets', '0') == '1'
|
||||
if not include_assets:
|
||||
if not self.instance or not include_assets:
|
||||
return []
|
||||
assets = self.instance.get_assets().only(
|
||||
"id", "hostname", "ip", "os",
|
||||
"org_id", "protocols",
|
||||
)
|
||||
"id", "hostname", "ip", "os", "platform_id",
|
||||
"org_id", "protocols", "is_active",
|
||||
).prefetch_related('platform')
|
||||
return self.serialize_assets(assets, self.instance.key)
|
||||
|
||||
|
||||
@@ -200,17 +223,13 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
nodes_id = request.data.get("nodes")
|
||||
children = [get_object_or_none(Node, id=pk) for pk in nodes_id]
|
||||
node_ids = request.data.get("nodes")
|
||||
children = Node.objects.filter(id__in=node_ids)
|
||||
for node in children:
|
||||
if not node:
|
||||
continue
|
||||
node.parent = instance
|
||||
return Response("OK")
|
||||
|
||||
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
|
||||
class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
@@ -223,8 +242,6 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
instance.assets.add(*tuple(assets))
|
||||
|
||||
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
|
||||
class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
@@ -237,12 +254,13 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
node.assets.remove(*assets)
|
||||
|
||||
# 把孤儿资产添加到 root 节点
|
||||
orphan_assets = Asset.objects.filter(id__in=[a.id for a in assets], nodes__isnull=True).distinct()
|
||||
orphan_assets = Asset.objects.filter(
|
||||
id__in=[a.id for a in assets],
|
||||
nodes__isnull=True
|
||||
).distinct()
|
||||
Node.org_root().assets.add(*orphan_assets)
|
||||
|
||||
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
|
||||
class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
|
||||
@@ -3,7 +3,8 @@ from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from common.drf.filters import CustomFilter
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import tmp_to_org
|
||||
@@ -12,7 +13,7 @@ from .. import serializers
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer
|
||||
from ..tasks import (
|
||||
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
|
||||
push_system_user_a_asset_manual,
|
||||
push_system_user_to_assets
|
||||
)
|
||||
|
||||
|
||||
@@ -28,8 +29,12 @@ class SystemUserViewSet(OrgBulkModelViewSet):
|
||||
System user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = SystemUser
|
||||
filter_fields = ("name", "username", "protocol")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'username': ['exact'],
|
||||
'protocol': ['exact', 'in']
|
||||
}
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.SystemUserSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.SystemUserSerializer,
|
||||
@@ -82,18 +87,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.SystemUserTaskSerializer
|
||||
|
||||
def do_push(self, system_user, asset=None):
|
||||
if asset is None:
|
||||
def do_push(self, system_user, asset_ids=None):
|
||||
if asset_ids is None:
|
||||
task = push_system_user_to_assets_manual.delay(system_user)
|
||||
else:
|
||||
username = self.request.query_params.get('username')
|
||||
task = push_system_user_a_asset_manual.delay(
|
||||
system_user, asset, username=username
|
||||
task = push_system_user_to_assets.delay(
|
||||
system_user.id, asset_ids, username=username
|
||||
)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def do_test(system_user, asset=None):
|
||||
def do_test(system_user):
|
||||
task = test_system_user_connectivity_manual.delay(system_user)
|
||||
return task
|
||||
|
||||
@@ -104,11 +109,16 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
asset = serializer.validated_data.get('asset')
|
||||
assets = serializer.validated_data.get('assets') or []
|
||||
|
||||
system_user = self.get_object()
|
||||
if action == 'push':
|
||||
task = self.do_push(system_user, asset)
|
||||
assets = [asset] if asset else assets
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
asset_ids = asset_ids if asset_ids else None
|
||||
task = self.do_push(system_user, asset_ids)
|
||||
else:
|
||||
task = self.do_test(system_user, asset)
|
||||
task = self.do_test(system_user)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
@@ -130,8 +140,8 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filter_fields = ("hostname", "ip")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("hostname", "ip")
|
||||
search_fields = filterset_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
||||
@@ -19,9 +19,10 @@ __all__ = [
|
||||
class RelationMixin:
|
||||
def get_queryset(self):
|
||||
queryset = self.model.objects.all()
|
||||
org_id = current_org.org_id()
|
||||
if org_id is not None:
|
||||
if not current_org.is_root():
|
||||
org_id = current_org.org_id()
|
||||
queryset = queryset.filter(systemuser__org_id=org_id)
|
||||
|
||||
queryset = queryset.annotate(systemuser_display=Concat(
|
||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
||||
Value(')')
|
||||
@@ -65,7 +66,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
@@ -91,7 +92,7 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
'id', 'node', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
@@ -112,7 +113,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserUserRelationSerializer
|
||||
model = models.SystemUser.users.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
filterset_fields = [
|
||||
'id', 'user', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
|
||||
def initial_some_nodes():
|
||||
from .models import Node
|
||||
Node.initial_some_nodes()
|
||||
|
||||
|
||||
def initial_some_nodes_callback(sender, **kwargs):
|
||||
initial_some_nodes()
|
||||
|
||||
|
||||
class AssetsConfig(AppConfig):
|
||||
@@ -19,7 +9,3 @@ class AssetsConfig(AppConfig):
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
try:
|
||||
initial_some_nodes()
|
||||
except Exception:
|
||||
post_migrate.connect(initial_some_nodes_callback, sender=self)
|
||||
|
||||
@@ -40,7 +40,7 @@ class BaseBackend:
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def make_assets_as_id(assets):
|
||||
def make_assets_as_ids(assets):
|
||||
if not assets:
|
||||
return []
|
||||
if isinstance(assets[0], Asset):
|
||||
|
||||
@@ -69,9 +69,9 @@ class DBBackend(BaseBackend):
|
||||
self.queryset = self.queryset.filter(union_id=union_id)
|
||||
|
||||
def _filter_assets(self, assets):
|
||||
assets_id = self.make_assets_as_id(assets)
|
||||
if assets_id:
|
||||
self.queryset = self.queryset.filter(asset_id__in=assets_id)
|
||||
asset_ids = self.make_assets_as_ids(assets)
|
||||
if asset_ids:
|
||||
self.queryset = self.queryset.filter(asset_id__in=asset_ids)
|
||||
|
||||
def _filter_node(self, node):
|
||||
pass
|
||||
@@ -165,7 +165,7 @@ class SystemUserBackend(DBBackend):
|
||||
kwargs = self.get_annotate()
|
||||
filters = self.get_filter()
|
||||
qs = self.model.objects.all().annotate(**kwargs)
|
||||
if current_org.org_id() is not None:
|
||||
if not current_org.is_root():
|
||||
filters['org_id'] = current_org.org_id()
|
||||
qs = qs.filter(**filters)
|
||||
qs = self.qs_to_values(qs)
|
||||
|
||||
20
apps/assets/locks.py
Normal file
20
apps/assets/locks.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from orgs.utils import current_org
|
||||
from common.utils.lock import DistributedLock
|
||||
|
||||
|
||||
class NodeTreeUpdateLock(DistributedLock):
|
||||
name_template = 'assets.node.tree.update.<org_id:{org_id}>'
|
||||
|
||||
def get_name(self):
|
||||
if current_org:
|
||||
org_id = current_org.id
|
||||
else:
|
||||
org_id = 'current_org_is_null'
|
||||
name = self.name_template.format(
|
||||
org_id=org_id
|
||||
)
|
||||
return name
|
||||
|
||||
def __init__(self):
|
||||
name = self.get_name()
|
||||
super().__init__(name=name, release_on_transaction_commit=True, reentrant=True)
|
||||
72
apps/assets/migrations/0063_migrate_default_node_key.py
Normal file
72
apps/assets/migrations/0063_migrate_default_node_key.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# Generated by Jiangjie.Bai on 2020-12-01 10:47
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Q
|
||||
|
||||
default_node_value = 'Default' # Always
|
||||
old_default_node_key = '0' # Version <= 1.4.3
|
||||
new_default_node_key = '1' # Version >= 1.4.4
|
||||
|
||||
|
||||
def compute_parent_key(key):
|
||||
try:
|
||||
return key[:key.rindex(':')]
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
|
||||
def migrate_default_node_key(apps, schema_editor):
|
||||
""" 将已经存在的Default节点的key从0修改为1 """
|
||||
# 1.4.3版本中Default节点的key为0
|
||||
print('')
|
||||
Node = apps.get_model('assets', 'Node')
|
||||
Asset = apps.get_model('assets', 'Asset')
|
||||
|
||||
# key为0的节点
|
||||
old_default_node = Node.objects.filter(key=old_default_node_key, value=default_node_value).first()
|
||||
if not old_default_node:
|
||||
print(f'Check old default node `key={old_default_node_key} value={default_node_value}` not exists')
|
||||
return
|
||||
print(f'Check old default node `key={old_default_node_key} value={default_node_value}` exists')
|
||||
# key为1的节点
|
||||
new_default_node = Node.objects.filter(key=new_default_node_key, value=default_node_value).first()
|
||||
if new_default_node:
|
||||
print(f'Check new default node `key={new_default_node_key} value={default_node_value}` exists')
|
||||
all_assets = Asset.objects.filter(
|
||||
Q(nodes__key__startswith=f'{new_default_node_key}:') | Q(nodes__key=new_default_node_key)
|
||||
).distinct()
|
||||
if all_assets:
|
||||
print(f'Check new default node has assets (count: {len(all_assets)})')
|
||||
return
|
||||
all_children = Node.objects.filter(key__startswith=f'{new_default_node_key}:')
|
||||
if all_children:
|
||||
print(f'Check new default node has children nodes (count: {len(all_children)})')
|
||||
return
|
||||
print(f'Check new default node not has assets and children nodes, delete it.')
|
||||
new_default_node.delete()
|
||||
# 执行修改
|
||||
print(f'Modify old default node `key` from `{old_default_node_key}` to `{new_default_node_key}`')
|
||||
nodes = Node.objects.filter(
|
||||
Q(key__istartswith=f'{old_default_node_key}:') | Q(key=old_default_node_key)
|
||||
)
|
||||
for node in nodes:
|
||||
old_key = node.key
|
||||
key_list = old_key.split(':', maxsplit=1)
|
||||
key_list[0] = new_default_node_key
|
||||
new_key = ':'.join(key_list)
|
||||
node.key = new_key
|
||||
node.parent_key = compute_parent_key(node.key)
|
||||
# 批量更新
|
||||
print(f'Bulk update nodes `key` and `parent_key`, (count: {len(nodes)})')
|
||||
Node.objects.bulk_update(nodes, ['key', 'parent_key'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0062_auto_20201117_1938'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_default_node_key)
|
||||
]
|
||||
17
apps/assets/migrations/0064_auto_20201203_1100.py
Normal file
17
apps/assets/migrations/0064_auto_20201203_1100.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1 on 2020-12-03 03:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0063_migrate_default_node_key'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='node',
|
||||
options={'ordering': ['parent_key', 'value'], 'verbose_name': 'Node'},
|
||||
),
|
||||
]
|
||||
17
apps/assets/migrations/0065_auto_20210121_1549.py
Normal file
17
apps/assets/migrations/0065_auto_20210121_1549.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1 on 2021-01-21 07:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0064_auto_20201203_1100'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='domain',
|
||||
options={'ordering': ('name',), 'verbose_name': 'Domain'},
|
||||
),
|
||||
]
|
||||
17
apps/assets/migrations/0066_auto_20210208_1802.py
Normal file
17
apps/assets/migrations/0066_auto_20210208_1802.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1 on 2021-02-08 10:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0065_auto_20210121_1549'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['hostname'], 'verbose_name': 'Asset'},
|
||||
),
|
||||
]
|
||||
48
apps/assets/migrations/0067_auto_20210311_1113.py
Normal file
48
apps/assets/migrations/0067_auto_20210311_1113.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.1 on 2021-03-11 03:13
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_cmd_filter_priority(apps, schema_editor):
|
||||
cmd_filter_rule_model = apps.get_model('assets', 'CommandFilterRule')
|
||||
cmd_filter_rules = cmd_filter_rule_model.objects.all()
|
||||
for cmd_filter_rule in cmd_filter_rules:
|
||||
cmd_filter_rule.priority = 100 - cmd_filter_rule.priority + 1
|
||||
|
||||
cmd_filter_rule_model.objects.bulk_update(cmd_filter_rules, fields=['priority'])
|
||||
|
||||
|
||||
def migrate_system_user_priority(apps, schema_editor):
|
||||
system_user_model = apps.get_model('assets', 'SystemUser')
|
||||
system_users = system_user_model.objects.all()
|
||||
for system_user in system_users:
|
||||
system_user.priority = 100 - system_user.priority + 1
|
||||
|
||||
system_user_model.objects.bulk_update(system_users, fields=['priority'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0066_auto_20210208_1802'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_cmd_filter_priority),
|
||||
migrations.RunPython(migrate_system_user_priority),
|
||||
migrations.AlterModelOptions(
|
||||
name='commandfilterrule',
|
||||
options={'ordering': ('priority', 'action'), 'verbose_name': 'Command filter rule'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='commandfilterrule',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=20, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
|
||||
),
|
||||
]
|
||||
19
apps/assets/migrations/0068_auto_20210312_1455.py
Normal file
19
apps/assets/migrations/0068_auto_20210312_1455.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1 on 2021-03-12 06:55
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0067_auto_20210311_1113'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=81, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
|
||||
),
|
||||
]
|
||||
@@ -17,7 +17,7 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from .base import ConnectivityMixin
|
||||
from .utils import Connectivity
|
||||
|
||||
__all__ = ['Asset', 'ProtocolsMixin', 'Platform']
|
||||
__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,13 +41,6 @@ def default_node():
|
||||
|
||||
|
||||
class AssetManager(OrgManager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().annotate(
|
||||
platform_base=models.F('platform__base')
|
||||
)
|
||||
|
||||
|
||||
class AssetOrgManager(OrgManager):
|
||||
pass
|
||||
|
||||
|
||||
@@ -230,7 +223,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
objects = AssetManager.from_queryset(AssetQuerySet)()
|
||||
org_objects = AssetOrgManager.from_queryset(AssetQuerySet)()
|
||||
_connectivity = None
|
||||
|
||||
def __str__(self):
|
||||
@@ -361,4 +353,4 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
ordering = ["hostname", "ip"]
|
||||
ordering = ["hostname", ]
|
||||
|
||||
@@ -57,7 +57,7 @@ class AuthBook(BaseUser):
|
||||
同时设置自己的 is_latest=True, version=max_version + 1
|
||||
"""
|
||||
username = kwargs['username']
|
||||
asset = kwargs['asset']
|
||||
asset = kwargs.get('asset') or kwargs.get('asset_id')
|
||||
with transaction.atomic():
|
||||
# 使用select_for_update限制并发创建相同的username、asset条目
|
||||
instances = cls.objects.select_for_update().filter(username=username, asset=asset)
|
||||
|
||||
@@ -11,9 +11,12 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.db.models import ChoiceSet
|
||||
from common.utils import random_string
|
||||
from common.utils import (
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
|
||||
)
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
@@ -105,6 +108,19 @@ class AuthMixin:
|
||||
username = ''
|
||||
_prefer = 'system_user'
|
||||
|
||||
@property
|
||||
def ssh_key_fingerprint(self):
|
||||
if self.public_key:
|
||||
public_key = self.public_key
|
||||
elif self.private_key:
|
||||
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
|
||||
else:
|
||||
return ''
|
||||
|
||||
public_key_obj = sshpubkeys.SSHKey(public_key)
|
||||
fingerprint = public_key_obj.hash_md5()
|
||||
return fingerprint
|
||||
|
||||
@property
|
||||
def private_key_obj(self):
|
||||
if self.private_key:
|
||||
@@ -204,8 +220,8 @@ class AuthMixin:
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
def gen_password():
|
||||
return str(uuid.uuid4())
|
||||
def gen_password(length=36):
|
||||
return random_string(length, special_char=True)
|
||||
|
||||
@staticmethod
|
||||
def gen_key(username):
|
||||
|
||||
@@ -50,7 +50,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
|
||||
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
|
||||
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
|
||||
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
||||
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
|
||||
@@ -60,7 +60,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
|
||||
|
||||
class Meta:
|
||||
ordering = ('-priority', 'action')
|
||||
ordering = ('priority', 'action')
|
||||
verbose_name = _("Command filter rule")
|
||||
|
||||
@lazyproperty
|
||||
|
||||
@@ -26,6 +26,7 @@ class Domain(OrgModelMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Domain")
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -16,17 +16,5 @@ class FavoriteAsset(CommonModelMixin):
|
||||
unique_together = ('user', 'asset')
|
||||
|
||||
@classmethod
|
||||
def get_user_favorite_assets_id(cls, user):
|
||||
def get_user_favorite_asset_ids(cls, user):
|
||||
return cls.objects.filter(user=user).values_list('asset', flat=True)
|
||||
|
||||
@classmethod
|
||||
def get_user_favorite_assets(cls, user, asset_perms_id=None):
|
||||
from assets.models import Asset
|
||||
from perms.utils.asset.user_permission import get_user_granted_all_assets
|
||||
asset_ids = get_user_granted_all_assets(
|
||||
user,
|
||||
via_mapping_node=False,
|
||||
asset_perms_id=asset_perms_id
|
||||
).values_list('id', flat=True)
|
||||
query_name = cls.asset.field.related_query_name()
|
||||
return Asset.org_objects.filter(**{f'{query_name}__user_id': user.id}, id__in=asset_ids).distinct()
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import threading
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Manager
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.db.transaction import atomic
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils.lock import DistributedLock
|
||||
from common.utils.common import timeit
|
||||
from common.db.models import output_as_string
|
||||
from common.utils import get_logger
|
||||
from common.utils.common import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from orgs.utils import get_current_org, tmp_to_org
|
||||
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key']
|
||||
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@@ -38,6 +47,7 @@ class FamilyMixin:
|
||||
__children = None
|
||||
__all_children = None
|
||||
is_node = True
|
||||
child_mark: int
|
||||
|
||||
@staticmethod
|
||||
def clean_children_keys(nodes_keys):
|
||||
@@ -103,7 +113,7 @@ class FamilyMixin:
|
||||
if value is None:
|
||||
value = child_key
|
||||
child = self.__class__.objects.create(
|
||||
id=_id, key=child_key, value=value, parent_key=self.key,
|
||||
id=_id, key=child_key, value=value
|
||||
)
|
||||
return child
|
||||
|
||||
@@ -121,11 +131,22 @@ class FamilyMixin:
|
||||
created = True
|
||||
return child, created
|
||||
|
||||
def get_valid_child_mark(self):
|
||||
key = "{}:{}".format(self.key, self.child_mark)
|
||||
if not self.__class__.objects.filter(key=key).exists():
|
||||
return self.child_mark
|
||||
children_keys = self.get_children().values_list('key', flat=True)
|
||||
children_keys_last = [key.split(':')[-1] for key in children_keys]
|
||||
children_keys_last = [int(k) for k in children_keys_last if k.strip().isdigit()]
|
||||
max_key_last = max(children_keys_last) if children_keys_last else 1
|
||||
return max_key_last + 1
|
||||
|
||||
def get_next_child_key(self):
|
||||
mark = self.child_mark
|
||||
self.child_mark += 1
|
||||
child_mark = self.get_valid_child_mark()
|
||||
key = "{}:{}".format(self.key, child_mark)
|
||||
self.child_mark = child_mark + 1
|
||||
self.save()
|
||||
return "{}:{}".format(self.key, mark)
|
||||
return key
|
||||
|
||||
def get_next_child_preset_name(self):
|
||||
name = ugettext("New node")
|
||||
@@ -235,9 +256,147 @@ class FamilyMixin:
|
||||
return [*tuple(ancestors), self, *tuple(children)]
|
||||
|
||||
|
||||
class NodeAssetsMixin:
|
||||
class NodeAllAssetsMappingMixin:
|
||||
# Use a new plan
|
||||
|
||||
# { org_id: { node_key: [ asset1_id, asset2_id ] } }
|
||||
orgid_nodekey_assetsid_mapping = defaultdict(dict)
|
||||
locks_for_get_mapping_from_cache = defaultdict(threading.Lock)
|
||||
|
||||
@classmethod
|
||||
def get_lock(cls, org_id):
|
||||
lock = cls.locks_for_get_mapping_from_cache[str(org_id)]
|
||||
return lock
|
||||
|
||||
@classmethod
|
||||
def get_node_all_asset_ids_mapping(cls, org_id):
|
||||
_mapping = cls.get_node_all_asset_ids_mapping_from_memory(org_id)
|
||||
if _mapping:
|
||||
return _mapping
|
||||
|
||||
logger.debug(f'Get node asset mapping from memory failed, acquire thread lock: '
|
||||
f'thread={threading.get_ident()} '
|
||||
f'org_id={org_id}')
|
||||
with cls.get_lock(org_id):
|
||||
logger.debug(f'Acquired thread lock ok. check if mapping is in memory now: '
|
||||
f'thread={threading.get_ident()} '
|
||||
f'org_id={org_id}')
|
||||
_mapping = cls.get_node_all_asset_ids_mapping_from_memory(org_id)
|
||||
if _mapping:
|
||||
logger.debug(f'Mapping is already in memory now: '
|
||||
f'thread={threading.get_ident()} '
|
||||
f'org_id={org_id}')
|
||||
return _mapping
|
||||
|
||||
_mapping = cls.get_node_all_asset_ids_mapping_from_cache_or_generate_to_cache(org_id)
|
||||
cls.set_node_all_asset_ids_mapping_to_memory(org_id, mapping=_mapping)
|
||||
return _mapping
|
||||
|
||||
# from memory
|
||||
@classmethod
|
||||
def get_node_all_asset_ids_mapping_from_memory(cls, org_id):
|
||||
mapping = cls.orgid_nodekey_assetsid_mapping.get(org_id, {})
|
||||
return mapping
|
||||
|
||||
@classmethod
|
||||
def set_node_all_asset_ids_mapping_to_memory(cls, org_id, mapping):
|
||||
cls.orgid_nodekey_assetsid_mapping[org_id] = mapping
|
||||
|
||||
@classmethod
|
||||
def expire_node_all_asset_ids_mapping_from_memory(cls, org_id):
|
||||
org_id = str(org_id)
|
||||
cls.orgid_nodekey_assetsid_mapping.pop(org_id, None)
|
||||
|
||||
# get order: from memory -> (from cache -> to generate)
|
||||
@classmethod
|
||||
def get_node_all_asset_ids_mapping_from_cache_or_generate_to_cache(cls, org_id):
|
||||
mapping = cls.get_node_all_asset_ids_mapping_from_cache(org_id)
|
||||
if mapping:
|
||||
return mapping
|
||||
|
||||
lock_key = f'KEY_LOCK_GENERATE_ORG_{org_id}_NODE_ALL_ASSET_ids_MAPPING'
|
||||
with DistributedLock(lock_key):
|
||||
# 这里使用无限期锁,原因是如果这里卡住了,就卡在数据库了,说明
|
||||
# 数据库繁忙,所以不应该再有线程执行这个操作,使数据库忙上加忙
|
||||
|
||||
_mapping = cls.get_node_all_asset_ids_mapping_from_cache(org_id)
|
||||
if _mapping:
|
||||
return _mapping
|
||||
|
||||
_mapping = cls.generate_node_all_asset_ids_mapping(org_id)
|
||||
cls.set_node_all_asset_ids_mapping_to_cache(org_id=org_id, mapping=_mapping)
|
||||
return _mapping
|
||||
|
||||
@classmethod
|
||||
def get_node_all_asset_ids_mapping_from_cache(cls, org_id):
|
||||
cache_key = cls._get_cache_key_for_node_all_asset_ids_mapping(org_id)
|
||||
mapping = cache.get(cache_key)
|
||||
logger.info(f'Get node asset mapping from cache {bool(mapping)}: '
|
||||
f'thread={threading.get_ident()} '
|
||||
f'org_id={org_id}')
|
||||
return mapping
|
||||
|
||||
@classmethod
|
||||
def set_node_all_asset_ids_mapping_to_cache(cls, org_id, mapping):
|
||||
cache_key = cls._get_cache_key_for_node_all_asset_ids_mapping(org_id)
|
||||
cache.set(cache_key, mapping, timeout=None)
|
||||
|
||||
@classmethod
|
||||
def expire_node_all_asset_ids_mapping_from_cache(cls, org_id):
|
||||
cache_key = cls._get_cache_key_for_node_all_asset_ids_mapping(org_id)
|
||||
cache.delete(cache_key)
|
||||
|
||||
@staticmethod
|
||||
def _get_cache_key_for_node_all_asset_ids_mapping(org_id):
|
||||
return 'ASSETS_ORG_NODE_ALL_ASSET_ids_MAPPING_{}'.format(org_id)
|
||||
|
||||
@classmethod
|
||||
def generate_node_all_asset_ids_mapping(cls, org_id):
|
||||
from .asset import Asset
|
||||
|
||||
logger.info(f'Generate node asset mapping: '
|
||||
f'thread={threading.get_ident()} '
|
||||
f'org_id={org_id}')
|
||||
t1 = time.time()
|
||||
with tmp_to_org(org_id):
|
||||
node_ids_key = Node.objects.annotate(
|
||||
char_id=output_as_string('id')
|
||||
).values_list('char_id', 'key')
|
||||
|
||||
# * 直接取出全部. filter(node__org_id=org_id)(大规模下会更慢)
|
||||
nodes_asset_ids = Asset.nodes.through.objects.all() \
|
||||
.annotate(char_node_id=output_as_string('node_id')) \
|
||||
.annotate(char_asset_id=output_as_string('asset_id')) \
|
||||
.values_list('char_node_id', 'char_asset_id')
|
||||
|
||||
node_id_ancestor_keys_mapping = {
|
||||
node_id: cls.get_node_ancestor_keys(node_key, with_self=True)
|
||||
for node_id, node_key in node_ids_key
|
||||
}
|
||||
|
||||
nodeid_assetsid_mapping = defaultdict(set)
|
||||
for node_id, asset_id in nodes_asset_ids:
|
||||
nodeid_assetsid_mapping[node_id].add(asset_id)
|
||||
|
||||
t2 = time.time()
|
||||
|
||||
mapping = defaultdict(set)
|
||||
for node_id, node_key in node_ids_key:
|
||||
asset_ids = nodeid_assetsid_mapping[node_id]
|
||||
node_ancestor_keys = node_id_ancestor_keys_mapping[node_id]
|
||||
for ancestor_key in node_ancestor_keys:
|
||||
mapping[ancestor_key].update(asset_ids)
|
||||
|
||||
t3 = time.time()
|
||||
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
|
||||
return mapping
|
||||
|
||||
|
||||
class NodeAssetsMixin(NodeAllAssetsMappingMixin):
|
||||
org_id: str
|
||||
key = ''
|
||||
id = None
|
||||
objects: Manager
|
||||
|
||||
def get_all_assets(self):
|
||||
from .asset import Asset
|
||||
@@ -251,8 +410,7 @@ class NodeAssetsMixin:
|
||||
# 可是 startswith 会导致表关联时 Asset 索引失效
|
||||
from .asset import Asset
|
||||
node_ids = cls.objects.filter(
|
||||
Q(key__startswith=f'{key}:') |
|
||||
Q(key=key)
|
||||
Q(key__startswith=f'{key}:') | Q(key=key)
|
||||
).values_list('id', flat=True).distinct()
|
||||
assets = Asset.objects.filter(
|
||||
nodes__id__in=list(node_ids)
|
||||
@@ -271,54 +429,42 @@ class NodeAssetsMixin:
|
||||
return self.get_all_assets().valid()
|
||||
|
||||
@classmethod
|
||||
def get_nodes_all_assets_ids(cls, nodes_keys):
|
||||
assets_ids = cls.get_nodes_all_assets(nodes_keys).values_list('id', flat=True)
|
||||
return assets_ids
|
||||
def get_nodes_all_asset_ids_by_keys(cls, nodes_keys):
|
||||
nodes = Node.objects.filter(key__in=nodes_keys)
|
||||
asset_ids = cls.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
return asset_ids
|
||||
|
||||
@classmethod
|
||||
def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None):
|
||||
def get_nodes_all_assets(cls, *nodes):
|
||||
from .asset import Asset
|
||||
nodes_keys = cls.clean_children_keys(nodes_keys)
|
||||
q = Q()
|
||||
node_ids = ()
|
||||
for key in nodes_keys:
|
||||
q |= Q(key__startswith=f'{key}:')
|
||||
q |= Q(key=key)
|
||||
if q:
|
||||
node_ids = Node.objects.filter(q).distinct().values_list('id', flat=True)
|
||||
node_ids = set()
|
||||
descendant_node_query = Q()
|
||||
for n in nodes:
|
||||
node_ids.add(n.id)
|
||||
descendant_node_query |= Q(key__istartswith=f'{n.key}:')
|
||||
if descendant_node_query:
|
||||
_ids = Node.objects.order_by().filter(descendant_node_query).values_list('id', flat=True)
|
||||
node_ids.update(_ids)
|
||||
return Asset.objects.order_by().filter(nodes__id__in=node_ids).distinct()
|
||||
|
||||
q = Q(nodes__id__in=list(node_ids))
|
||||
if extra_assets_ids:
|
||||
q |= Q(id__in=extra_assets_ids)
|
||||
if q:
|
||||
return Asset.org_objects.filter(q).distinct()
|
||||
else:
|
||||
return Asset.objects.none()
|
||||
def get_all_asset_ids(self):
|
||||
asset_ids = self.get_all_asset_ids_by_node_key(org_id=self.org_id, node_key=self.key)
|
||||
return set(asset_ids)
|
||||
|
||||
@classmethod
|
||||
def get_all_asset_ids_by_node_key(cls, org_id, node_key):
|
||||
org_id = str(org_id)
|
||||
nodekey_assetsid_mapping = cls.get_node_all_asset_ids_mapping(org_id)
|
||||
asset_ids = nodekey_assetsid_mapping.get(node_key, [])
|
||||
return set(asset_ids)
|
||||
|
||||
|
||||
class SomeNodesMixin:
|
||||
key = ''
|
||||
default_key = '1'
|
||||
default_value = 'Default'
|
||||
empty_key = '-11'
|
||||
empty_value = _("empty")
|
||||
|
||||
@classmethod
|
||||
def default_node(cls):
|
||||
with tmp_to_org(Organization.default()):
|
||||
defaults = {'value': cls.default_value}
|
||||
try:
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.error("Create default node failed: {}".format(e))
|
||||
cls.modify_other_org_root_node_key()
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
return obj
|
||||
|
||||
def is_default_node(self):
|
||||
return self.key == self.default_key
|
||||
|
||||
@@ -329,69 +475,61 @@ class SomeNodesMixin:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_next_org_root_node_key(cls):
|
||||
with tmp_to_org(Organization.root()):
|
||||
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
|
||||
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True)
|
||||
if not org_nodes_roots_keys:
|
||||
org_nodes_roots_keys = ['1']
|
||||
max_key = max([int(k) for k in org_nodes_roots_keys])
|
||||
key = str(max_key + 1) if max_key != 0 else '2'
|
||||
return key
|
||||
def org_root(cls):
|
||||
# 如果使用current_org 在set_current_org时会死循环
|
||||
ori_org = get_current_org()
|
||||
|
||||
if ori_org and ori_org.is_default():
|
||||
return cls.default_node()
|
||||
|
||||
if ori_org and ori_org.is_root():
|
||||
return None
|
||||
|
||||
org_roots = cls.org_root_nodes()
|
||||
org_roots_length = len(org_roots)
|
||||
|
||||
if org_roots_length == 1:
|
||||
root = org_roots[0]
|
||||
return root
|
||||
elif org_roots_length == 0:
|
||||
root = cls.create_org_root_node()
|
||||
return root
|
||||
else:
|
||||
error = 'Current org {} root node not 1, get {}'.format(ori_org, org_roots_length)
|
||||
raise ValueError(error)
|
||||
|
||||
@classmethod
|
||||
def default_node(cls):
|
||||
default_org = Organization.default()
|
||||
with tmp_to_org(default_org):
|
||||
defaults = {'value': default_org.name}
|
||||
obj, created = cls.objects.get_or_create(defaults=defaults, key=cls.default_key)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def create_org_root_node(cls):
|
||||
# 如果使用current_org 在set_current_org时会死循环
|
||||
ori_org = get_current_org()
|
||||
with transaction.atomic():
|
||||
if not ori_org.is_real():
|
||||
return cls.default_node()
|
||||
key = cls.get_next_org_root_node_key()
|
||||
root = cls.objects.create(key=key, value=ori_org.name)
|
||||
return root
|
||||
|
||||
@classmethod
|
||||
def org_root(cls):
|
||||
root = cls.objects.filter(parent_key='')\
|
||||
.filter(key__regex=r'^[0-9]+$')\
|
||||
.exclude(key__startswith='-')
|
||||
if root:
|
||||
return root[0]
|
||||
else:
|
||||
return cls.create_org_root_node()
|
||||
def get_next_org_root_node_key(cls):
|
||||
with tmp_to_root_org():
|
||||
org_nodes_roots = cls.org_root_nodes()
|
||||
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True)
|
||||
if not org_nodes_roots_keys:
|
||||
org_nodes_roots_keys = ['1']
|
||||
max_key = max([int(k) for k in org_nodes_roots_keys])
|
||||
key = str(max_key + 1) if max_key > 0 else '2'
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def initial_some_nodes(cls):
|
||||
cls.default_node()
|
||||
|
||||
@classmethod
|
||||
def modify_other_org_root_node_key(cls):
|
||||
"""
|
||||
解决创建 default 节点失败的问题,
|
||||
因为在其他组织下存在 default 节点,故在 DEFAULT 组织下 get 不到 create 失败
|
||||
"""
|
||||
logger.info("Modify other org root node key")
|
||||
|
||||
with tmp_to_org(Organization.root()):
|
||||
node_key1 = cls.objects.filter(key='1').first()
|
||||
if not node_key1:
|
||||
logger.info("Not found node that `key` = 1")
|
||||
return
|
||||
if not node_key1.org.is_real():
|
||||
logger.info("Org is not real for node that `key` = 1")
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
with tmp_to_org(node_key1.org):
|
||||
org_root_node_new_key = cls.get_next_org_root_node_key()
|
||||
for n in cls.objects.all():
|
||||
old_key = n.key
|
||||
key_list = n.key.split(':')
|
||||
key_list[0] = org_root_node_new_key
|
||||
new_key = ':'.join(key_list)
|
||||
n.key = new_key
|
||||
n.save()
|
||||
logger.info('Modify key ( {} > {} )'.format(old_key, new_key))
|
||||
def org_root_nodes(cls):
|
||||
root_nodes = cls.objects.filter(parent_key='', key__regex=r'^[0-9]+$') \
|
||||
.exclude(key__startswith='-').order_by('key')
|
||||
return root_nodes
|
||||
|
||||
|
||||
class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
@@ -411,7 +549,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Node")
|
||||
ordering = ['value']
|
||||
ordering = ['parent_key', 'value']
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
@@ -490,10 +628,15 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
sort_key_func = lambda n: [int(i) for i in n.key.split(':')]
|
||||
nodes_sorted = sorted(list(nodes), key=sort_key_func)
|
||||
nodes_mapper = {n.key: n for n in nodes_sorted}
|
||||
if not self.is_org_root():
|
||||
# 如果是org_root,那么parent_key为'', parent为自己,所以这种情况不处理
|
||||
# 更新自己时,自己的parent_key获取不到
|
||||
nodes_mapper.update({self.parent_key: self.parent})
|
||||
for node in nodes_sorted:
|
||||
parent = nodes_mapper.get(node.parent_key)
|
||||
if not parent:
|
||||
logger.error(f'Node parent node in mapper: {node.parent_key} {node.value}')
|
||||
if node.parent_key:
|
||||
logger.error(f'Node parent node in mapper: {node.parent_key} {node.value}')
|
||||
continue
|
||||
node.full_value = parent.full_value + '/' + node.value
|
||||
self.__class__.objects.bulk_update(nodes, ['full_value'])
|
||||
|
||||
@@ -88,6 +88,26 @@ class SystemUser(BaseUser):
|
||||
(PROTOCOL_K8S, 'k8s'),
|
||||
)
|
||||
|
||||
SUPPORT_PUSH_PROTOCOLS = [PROTOCOL_SSH, PROTOCOL_RDP]
|
||||
|
||||
ASSET_CATEGORY_PROTOCOLS = [
|
||||
PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC
|
||||
]
|
||||
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
|
||||
PROTOCOL_RDP
|
||||
]
|
||||
APPLICATION_CATEGORY_DB_PROTOCOLS = [
|
||||
PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL
|
||||
]
|
||||
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
|
||||
PROTOCOL_K8S
|
||||
]
|
||||
APPLICATION_CATEGORY_PROTOCOLS = [
|
||||
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
|
||||
*APPLICATION_CATEGORY_DB_PROTOCOLS,
|
||||
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
|
||||
]
|
||||
|
||||
LOGIN_AUTO = 'auto'
|
||||
LOGIN_MANUAL = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
@@ -99,7 +119,7 @@ class SystemUser(BaseUser):
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
|
||||
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
@@ -133,37 +153,23 @@ class SystemUser(BaseUser):
|
||||
def login_mode_display(self):
|
||||
return self.get_login_mode_display()
|
||||
|
||||
@property
|
||||
def db_application_protocols(self):
|
||||
return [
|
||||
self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB,
|
||||
self.PROTOCOL_POSTGRESQL
|
||||
]
|
||||
|
||||
@property
|
||||
def cloud_application_protocols(self):
|
||||
return [self.PROTOCOL_K8S]
|
||||
|
||||
@property
|
||||
def application_category_protocols(self):
|
||||
protocols = []
|
||||
protocols.extend(self.db_application_protocols)
|
||||
protocols.extend(self.cloud_application_protocols)
|
||||
return protocols
|
||||
|
||||
def is_need_push(self):
|
||||
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
|
||||
if self.auto_push and self.is_protocol_support_push:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_protocol_support_push(self):
|
||||
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
|
||||
|
||||
@property
|
||||
def is_need_cmd_filter(self):
|
||||
return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC]
|
||||
|
||||
@property
|
||||
def is_need_test_asset_connective(self):
|
||||
return self.protocol not in self.application_category_protocols
|
||||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
def has_special_auth(self, asset=None, username=None):
|
||||
if username is None and self.username_same_with_user:
|
||||
@@ -172,7 +178,7 @@ class SystemUser(BaseUser):
|
||||
|
||||
@property
|
||||
def can_perm_to_asset(self):
|
||||
return self.protocol not in self.application_category_protocols
|
||||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
def _merge_auth(self, other):
|
||||
super()._merge_auth(other)
|
||||
@@ -199,12 +205,23 @@ class SystemUser(BaseUser):
|
||||
def get_all_assets(self):
|
||||
from assets.models import Node
|
||||
nodes_keys = self.nodes.all().values_list('key', flat=True)
|
||||
assets_ids = set(self.assets.all().values_list('id', flat=True))
|
||||
nodes_assets_ids = Node.get_nodes_all_assets_ids(nodes_keys)
|
||||
assets_ids.update(nodes_assets_ids)
|
||||
assets = Asset.objects.filter(id__in=assets_ids)
|
||||
asset_ids = set(self.assets.all().values_list('id', flat=True))
|
||||
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
|
||||
asset_ids.update(nodes_asset_ids)
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def get_protocol_by_application_type(cls, app_type):
|
||||
from applications.const import ApplicationTypeChoices
|
||||
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
|
||||
protocol = app_type
|
||||
elif app_type in ApplicationTypeChoices.remote_app_types():
|
||||
protocol = cls.PROTOCOL_RDP
|
||||
else:
|
||||
protocol = None
|
||||
return protocol
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
|
||||
@@ -1,39 +1,52 @@
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.utils import get_logger
|
||||
from assets.models import Node
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AssetPaginationBase(LimitOffsetPagination):
|
||||
|
||||
def init_attrs(self, queryset, request: Request, view=None):
|
||||
self._request = request
|
||||
self._view = view
|
||||
self._user = request.user
|
||||
|
||||
def paginate_queryset(self, queryset, request: Request, view=None):
|
||||
self.init_attrs(queryset, request, view)
|
||||
return super().paginate_queryset(queryset, request, view=None)
|
||||
|
||||
class AssetLimitOffsetPagination(LimitOffsetPagination):
|
||||
"""
|
||||
需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用
|
||||
"""
|
||||
def get_count(self, queryset):
|
||||
"""
|
||||
1. 如果查询节点下的所有资产,那 count 使用 Node.assets_amount
|
||||
2. 如果有其他过滤条件使用 super
|
||||
3. 如果只查询该节点下的资产使用 super
|
||||
"""
|
||||
exclude_query_params = {
|
||||
self.limit_query_param,
|
||||
self.offset_query_param,
|
||||
'node', 'all', 'show_current_asset',
|
||||
'node_id', 'display', 'draw', 'fields_size',
|
||||
'key', 'all', 'show_current_asset',
|
||||
'cache_policy', 'display', 'draw',
|
||||
'order', 'node', 'node_id', 'fields_size',
|
||||
}
|
||||
|
||||
for k, v in self._request.query_params.items():
|
||||
if k not in exclude_query_params and v is not None:
|
||||
logger.warn(f'Not hit node.assets_amount because find a unknow query_param `{k}` -> {self._request.get_full_path()}')
|
||||
return super().get_count(queryset)
|
||||
node_assets_count = self.get_count_from_nodes(queryset)
|
||||
if node_assets_count is None:
|
||||
return super().get_count(queryset)
|
||||
return node_assets_count
|
||||
|
||||
def get_count_from_nodes(self, queryset):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NodeAssetTreePagination(AssetPaginationBase):
|
||||
def get_count_from_nodes(self, queryset):
|
||||
is_query_all = self._view.is_query_node_all_assets
|
||||
if is_query_all:
|
||||
node = self._view.node
|
||||
if not node:
|
||||
node = Node.org_root()
|
||||
return node.assets_amount
|
||||
return super().get_count(queryset)
|
||||
|
||||
def paginate_queryset(self, queryset, request: Request, view=None):
|
||||
self._request = request
|
||||
self._view = view
|
||||
return super().paginate_queryset(queryset, request, view=None)
|
||||
if node:
|
||||
logger.debug(f'Hit node.assets_amount[{node.assets_amount}] -> {self._request.get_full_path()}')
|
||||
return node.assets_amount
|
||||
return None
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from ..models import Node, AdminUser
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
@@ -17,7 +15,6 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
model = AdminUser
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'private_key', 'public_key',
|
||||
@@ -26,6 +23,7 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
|
||||
|
||||
extra_kwargs = {
|
||||
'username': {"required": True},
|
||||
'password': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
@@ -33,6 +31,11 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class AdminUserDetailSerializer(AdminUserSerializer):
|
||||
class Meta(AdminUserSerializer.Meta):
|
||||
fields = AdminUserSerializer.Meta.fields + ['ssh_key_fingerprint']
|
||||
|
||||
|
||||
class AdminUserAuthSerializer(AuthSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.db.models import F
|
||||
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
@@ -98,9 +98,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
fields_as = list(annotates_fields.keys())
|
||||
fields = fields_small + fields_fk + fields_m2m + fields_as
|
||||
read_only_fields = [
|
||||
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
|
||||
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
|
||||
'os', 'os_version', 'os_arch', 'hostname_raw',
|
||||
'created_by', 'date_created',
|
||||
] + fields_as
|
||||
|
||||
@@ -114,7 +111,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.select_related('admin_user', 'domain', 'platform')
|
||||
queryset = queryset.prefetch_related('admin_user', 'domain', 'platform')
|
||||
queryset = queryset.prefetch_related('nodes', 'labels')
|
||||
return queryset
|
||||
|
||||
@@ -169,16 +166,17 @@ class AssetDisplaySerializer(AssetSerializer):
|
||||
'connectivity',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = super().setup_eager_loading(queryset)
|
||||
queryset = queryset\
|
||||
.annotate(admin_user_username=F('admin_user__username'))
|
||||
return queryset
|
||||
|
||||
|
||||
class PlatformSerializer(serializers.ModelSerializer):
|
||||
meta = serializers.DictField(required=False, allow_null=True)
|
||||
meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# TODO 修复 drf SlugField RegexValidator bug,之后记得删除
|
||||
validators = self.fields['name'].validators
|
||||
if isinstance(validators[-1], RegexValidator):
|
||||
validators.pop()
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
@@ -207,3 +205,6 @@ class AssetTaskSerializer(serializers.Serializer):
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
assets = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import AuthBook, Asset
|
||||
from ..backends import AssetUserManager
|
||||
|
||||
@@ -41,10 +41,6 @@ class AuthSerializerMixin:
|
||||
def validate_private_key(self, private_key):
|
||||
if not private_key:
|
||||
return
|
||||
if 'OPENSSH' in private_key:
|
||||
msg = _("Not support openssh format key, using "
|
||||
"ssh-keygen -t rsa -m pem to generate")
|
||||
raise serializers.ValidationError(msg)
|
||||
password = self.initial_data.get("password")
|
||||
valid = validate_ssh_private_key(private_key, password)
|
||||
if not valid:
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.fields import ChoiceDisplayField
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from ..models import CommandFilter, CommandFilterRule, SystemUser
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
@@ -26,7 +25,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
# serializer_choice_field = ChoiceDisplayField
|
||||
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user